From 672443940cfa265fe9ac258b5b80e8c229a841f4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 14 Sep 2023 17:24:08 +0500 Subject: [PATCH] Implemented experiment info calls for flutter (#131) * Implemented experiment info calls for flutter -- Added android native calls for experiment info -- Added iOS native calls for experiment info -- Updated underlying SDK's * Updated changelog * iOS: renamed 'CountlyExperimentInformation' class * renamed 'ExperimentInformation' class * updated native file after renaming --- CHANGELOG.md | 6 +- android/build.gradle | 2 +- .../countly_flutter/CountlyFlutterPlugin.java | 37 ++++- example-no-push/android/app/build.gradle | 2 +- example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- example/ios/Flutter/Flutter.podspec | 4 +- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 6 +- example/ios/Runner.xcodeproj/project.pbxproj | 36 ++++- example/lib/main.dart | 11 ++ ios/Classes/CountlyFlutterPlugin.m | 33 ++++- ios/Classes/CountlyiOS/CHANGELOG.md | 10 ++ ios/Classes/CountlyiOS/Countly-PL.podspec | 4 +- ios/Classes/CountlyiOS/Countly.podspec | 4 +- ios/Classes/CountlyiOS/CountlyCommon.m | 2 +- .../CountlyiOS/CountlyExperimentInfo.h | 23 +++ .../CountlyiOS/CountlyExperimentInfo.m | 44 ++++++ .../CountlyiOS/CountlyExperimentInformation.h | 23 +++ .../CountlyiOS/CountlyExperimentInformation.m | 44 ++++++ .../CountlyiOS/CountlyFeedbackWidget.h | 3 + .../CountlyiOS/CountlyFeedbackWidget.m | 12 +- ios/Classes/CountlyiOS/CountlyRemoteConfig.h | 4 + ios/Classes/CountlyiOS/CountlyRemoteConfig.m | 13 ++ .../CountlyiOS/CountlyRemoteConfigInternal.h | 3 + .../CountlyiOS/CountlyRemoteConfigInternal.m | 138 ++++++++++++++++++ lib/experiment_information.dart | 25 ++++ lib/remote_config.dart | 6 + lib/remote_config_internal.dart | 29 ++++ scripts/no-push-files/build.gradle | 2 +- 30 files changed, 504 insertions(+), 28 deletions(-) create mode 100644 ios/Classes/CountlyiOS/CountlyExperimentInfo.h create mode 100644 ios/Classes/CountlyiOS/CountlyExperimentInfo.m create mode 100644 ios/Classes/CountlyiOS/CountlyExperimentInformation.h create mode 100644 ios/Classes/CountlyiOS/CountlyExperimentInformation.m create mode 100644 lib/experiment_information.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5930c6..16d3410f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## 23.8.1 * Added `enrollABOnRCDownload` config method to enroll users to AB tests when downloading Remote Config values -* Fixed a bug where enabling consent requirements would enable consents for all features for Android +* Fixed a bug where enabling consent requirements would enable consents for all features for Android +* Added `testingDownloadExperimentInformation:` in remote config interface +* Added `testingGetAllExperimentInfo:` in remote config interface +* Updated underlying Android SDK version to 23.8.1 +* Updated underlying iOS SDK version to 23.8.2 ## 23.8.0 * ! Minor breaking change ! Manual view recording calls are now ignored if automatic view recording mode is enabled. diff --git a/android/build.gradle b/android/build.gradle index ce7a5ec7..13278c1c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -34,6 +34,6 @@ android { } dependencies { - implementation 'ly.count.android:sdk:23.8.0' + implementation 'ly.count.android:sdk:23.8.1' implementation 'com.google.firebase:firebase-messaging:20.2.1' } diff --git a/android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java b/android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java index d665ebd7..79f464be 100644 --- a/android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java +++ b/android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java @@ -13,6 +13,7 @@ import ly.count.android.sdk.Countly; import ly.count.android.sdk.CountlyConfig; +import ly.count.android.sdk.ExperimentInformation; import ly.count.android.sdk.FeedbackRatingCallback; import ly.count.android.sdk.ModuleFeedback.*; import ly.count.android.sdk.DeviceIdType; @@ -981,7 +982,41 @@ public void callback(RequestResult downloadResult, String error, boolean fullVal }); result.success(null); - } else if ("remoteConfigTestingEnrollIntoVariant".equals(call.method)) { + } + else if ("testingDownloadExperimentInformation".equals(call.method)) { + int requestID = args.getInt(0); + + log("testingDownloadExperimentInformation", LogLevel.WARNING); + + Countly.sharedInstance().remoteConfig().testingDownloadExperimentInformation((rResult, error) -> { + if (requestID == requestIDNoCallback) { + return; + } + Map data = new HashMap<>(); + data.put("error", error); + data.put("requestResult", resultResponder(rResult)); + data.put("id", requestID); + methodChannel.invokeMethod("remoteConfigVariantCallback", data); + }); + + result.success(null); + } + else if ("testingGetAllExperimentInfo".equals(call.method)) { + Map experimentInfoMap = Countly.sharedInstance().remoteConfig().testingGetAllExperimentInfo(); + List> experimentInfoArray = new ArrayList<>(); + for (Map.Entry entry : experimentInfoMap.entrySet()) { + ExperimentInformation experimentInfo = entry.getValue(); + Map experimentInfoValue = new HashMap<>(); + experimentInfoValue.put("experimentID", experimentInfo.experimentName); + experimentInfoValue.put("experimentName", experimentInfo.experimentName); + experimentInfoValue.put("experimentDescription", experimentInfo.experimentDescription); + experimentInfoValue.put("currentVariant", experimentInfo.currentVariant); + experimentInfoValue.put("variants", experimentInfo.variants); + experimentInfoArray.add(experimentInfoValue); + } + result.success(experimentInfoArray); + } + else if ("remoteConfigTestingEnrollIntoVariant".equals(call.method)) { int requestID = args.getInt(0); String key = args.getString(1); String variant = args.getString(2); diff --git a/example-no-push/android/app/build.gradle b/example-no-push/android/app/build.gradle index ca37083a..8c931853 100644 --- a/example-no-push/android/app/build.gradle +++ b/example-no-push/android/app/build.gradle @@ -59,6 +59,6 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'ly.count.android:sdk:22.09.4' + implementation 'ly.count.android:sdk:23.8.1' } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 88c2de51..00156f08 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -59,7 +59,7 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'ly.count.android:sdk:22.09.4' + implementation 'ly.count.android:sdk:23.8.1' implementation 'com.google.firebase:firebase-messaging:20.2.1' implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) } diff --git a/example/android/build.gradle b/example/android/build.gradle index 50d5d9a1..8f378b69 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -25,6 +25,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/ios/Flutter/Flutter.podspec b/example/ios/Flutter/Flutter.podspec index 8ce43943..29758b70 100644 --- a/example/ios/Flutter/Flutter.podspec +++ b/example/ios/Flutter/Flutter.podspec @@ -1,6 +1,6 @@ # -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# This is a generated file; do not edit or check into version control. +# This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. # Pod::Spec.new do |s| diff --git a/example/ios/Podfile b/example/ios/Podfile index 88359b22..313ea4a1 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8161ce99..f06f0471 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - countly_flutter (23.6.0): + - countly_flutter (23.8.0): - Flutter - Flutter (1.0.0) @@ -14,9 +14,9 @@ EXTERNAL SOURCES: :path: Flutter SPEC CHECKSUMS: - countly_flutter: 4eeee607183664b871589250a0bd049cfd2697eb + countly_flutter: f153e5547d4f3cdf24be11f6ed4df32c9a421fa3 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 +PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d COCOAPODS: 1.12.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0a41c084..48e65e40 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -462,7 +462,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -490,7 +494,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = CountlyNSE/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.countly.demo.CountlyNSE; @@ -514,7 +522,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = CountlyNSE/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.countly.demo.CountlyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -537,7 +549,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = CountlyNSE/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.countly.demo.CountlyNSE; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -666,7 +682,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -696,7 +716,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/example/lib/main.dart b/example/lib/main.dart index 72ffb5b1..1e4d2292 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:countly_flutter/countly_flutter.dart'; +import 'package:countly_flutter/experiment_information.dart'; import 'package:flutter/material.dart'; final navigatorKey = GlobalKey(); @@ -150,6 +151,15 @@ class _MyAppState extends State { Countly.changeDeviceId(Countly.deviceIDType['TemporaryDeviceID']!, false); } + void remoteConfigDownloadExperimentInfo() { + Countly.instance.remoteConfig.testingDownloadExperimentInformation((rResult, error) async { + if(rResult == RequestResult.success) { + Map experimentInfoMap = await Countly.instance.remoteConfig.testingGetAllExperimentInfo(); + print(experimentInfoMap); + } + }); + } + void remoteConfigRegisterDownloadCallback() { Countly.instance.remoteConfig.registerDownloadCallback(_rcDownloadCallback); } @@ -1080,6 +1090,7 @@ class _MyAppState extends State { MyButton(text: 'Get AB testing values (Legacy)', color: 'green', onPressed: getABTestingValues), MyButton(text: 'Record event for goal #1', color: 'green', onPressed: eventForGoal_1), MyButton(text: 'Record event for goal #2', color: 'green', onPressed: eventForGoal_2), + MyButton(text: 'Remote Config Download Experiment Info', color: 'purple', onPressed: remoteConfigDownloadExperimentInfo), MyButton(text: 'Remote Config Register Download Callback', color: 'purple', onPressed: remoteConfigRegisterDownloadCallback), MyButton(text: 'Remote Config Remove Download Callback', color: 'purple', onPressed: remoteConfigRemoveDownloadCallback), MyButton(text: 'Remote Config Download Values', color: 'purple', onPressed: remoteConfigDownloadKeys), diff --git a/ios/Classes/CountlyFlutterPlugin.m b/ios/Classes/CountlyFlutterPlugin.m index 5f3ad9be..2621f985 100644 --- a/ios/Classes/CountlyFlutterPlugin.m +++ b/ios/Classes/CountlyFlutterPlugin.m @@ -930,7 +930,38 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result result(@"Success!"); }); - } else if ([@"presentRatingWidgetWithID" isEqualToString:call.method]) { + } else if ([@"testingDownloadExperimentInformation" isEqualToString:call.method]) { + NSNumber *callbackID = [command objectAtIndex:0]; + dispatch_async(dispatch_get_main_queue(), ^{ + [Countly.sharedInstance.remoteConfig testingDownloadExperimentInformation:^(CLYRequestResult _Nonnull response, NSError * _Nonnull error) { + [self remoteConfigVariantCallback:callbackID response:response error:error]; + }]; + result(@"Success!"); + }); + + } else if ([@"testingGetAllExperimentInfo" isEqualToString:call.method]) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary * experiments = [Countly.sharedInstance.remoteConfig testingGetAllExperimentInfo]; + NSMutableArray *experimentInfoArray = [NSMutableArray arrayWithCapacity:experiments.count]; + [experiments enumerateKeysAndObjectsUsingBlock:^(NSString * key, CountlyExperimentInformation* experimentID, BOOL * stop) + { + NSMutableDictionary *experimentInfoValue = [NSMutableDictionary dictionaryWithCapacity:5]; + experimentInfoValue[@"experimentID"] = experimentID.experimentID; + experimentInfoValue[@"experimentName"] = experimentID.experimentName; + experimentInfoValue[@"experimentDescription"] = experimentID.experimentDescription; + if(experimentID.currentVariant) { + experimentInfoValue[@"currentVariant"] = experimentID.currentVariant; + } + else + { + experimentInfoValue[@"currentVariant"] = @"null"; + } + experimentInfoValue[@"variants"] = experimentID.variants; + [experimentInfoArray addObject:experimentInfoValue]; + }]; + result(experimentInfoArray); + }); + } else if ([@"presentRatingWidgetWithID" isEqualToString:call.method]) { dispatch_async(dispatch_get_main_queue(), ^{ NSString *widgetId = [command objectAtIndex:0]; [Countly.sharedInstance presentRatingWidgetWithID:widgetId diff --git a/ios/Classes/CountlyiOS/CHANGELOG.md b/ios/Classes/CountlyiOS/CHANGELOG.md index 64b68d32..e4976567 100644 --- a/ios/Classes/CountlyiOS/CHANGELOG.md +++ b/ios/Classes/CountlyiOS/CHANGELOG.md @@ -1,3 +1,13 @@ +## 23.8.2 +- Fixed rating feedback widget event key for widget closed event +- Added `testingDownloadExperimentInformation:` in remote config interface +- Added `testingGetAllExperimentInfo:` in remote config interface + +## 23.8.1 +- Expanded feedback widget functionality. Added ability to use rating widgets. +- Added functionality to access tags for feedback widgets. +- Fixed SPM public header issues of `CountlyViewTracking.h` + ## 23.8.0 - Added `CountlyViewTracking:` interface with new view methods: - `setGlobalViewSegmentation:` diff --git a/ios/Classes/CountlyiOS/Countly-PL.podspec b/ios/Classes/CountlyiOS/Countly-PL.podspec index 4767d3f9..eecb8ca6 100644 --- a/ios/Classes/CountlyiOS/Countly-PL.podspec +++ b/ios/Classes/CountlyiOS/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '23.8.0' + s.version = '23.8.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.subspec 'Core' do |core| core.source_files = '*.{h,m}' - core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h' + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h' core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/ios/Classes/CountlyiOS/Countly.podspec b/ios/Classes/CountlyiOS/Countly.podspec index 3b781366..e2c39e1f 100644 --- a/ios/Classes/CountlyiOS/Countly.podspec +++ b/ios/Classes/CountlyiOS/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '23.8.0' + s.version = '23.8.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.subspec 'Core' do |core| core.source_files = '*.{h,m}' - core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h' + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h' core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/ios/Classes/CountlyiOS/CountlyCommon.m b/ios/Classes/CountlyiOS/CountlyCommon.m index 21a5d398..265b3e10 100644 --- a/ios/Classes/CountlyiOS/CountlyCommon.m +++ b/ios/Classes/CountlyiOS/CountlyCommon.m @@ -26,7 +26,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"23.8.0"; +NSString* const kCountlySDKVersion = @"23.8.2"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; diff --git a/ios/Classes/CountlyiOS/CountlyExperimentInfo.h b/ios/Classes/CountlyiOS/CountlyExperimentInfo.h new file mode 100644 index 00000000..ddc2ac98 --- /dev/null +++ b/ios/Classes/CountlyiOS/CountlyExperimentInfo.h @@ -0,0 +1,23 @@ +// CountlyExperimentInfo.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +@interface CountlyExperimentInfo : NSObject + +@property (nonatomic, readonly) NSString* experimentID; +@property (nonatomic, readonly) NSString* experimentName; +@property (nonatomic, readonly) NSString* experimentDescription; +@property (nonatomic, readonly) NSString* currentVariant; +@property (nonatomic, readonly) NSDictionary* variants; + + +- (instancetype)initWithID:(NSString*)experimentID experimentName:(NSString*)experimentName experimentDescription:(NSString*)experimentDescription currentVariant:(NSString*)currentVariant variants:(NSDictionary*)variants; + +@end + + + diff --git a/ios/Classes/CountlyiOS/CountlyExperimentInfo.m b/ios/Classes/CountlyiOS/CountlyExperimentInfo.m new file mode 100644 index 00000000..63b0d37d --- /dev/null +++ b/ios/Classes/CountlyiOS/CountlyExperimentInfo.m @@ -0,0 +1,44 @@ +// CountlyExperimentInfo.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyExperimentInfo.h" + +@interface CountlyExperimentInfo () +@property (nonatomic) NSString* experimentID; +@property (nonatomic) NSString* experimentName; +@property (nonatomic) NSString* experimentDescription; +@property (nonatomic) NSString* currentVariant; +@property (nonatomic) NSDictionary* variants; +@end + + +@implementation CountlyExperimentInfo + +- (instancetype)init +{ + if (self = [super init]) + { + } + + return self; +} + +- (instancetype)initWithID:(NSString*)experimentID experimentName:(NSString*)experimentName experimentDescription:(NSString*)experimentDescription currentVariant:(NSString*)currentVariant variants:(NSDictionary*)variants +{ + if (self = [super init]) + { + self.experimentID = experimentID; + self.experimentName = experimentName; + self.experimentDescription = experimentDescription; + self.currentVariant = currentVariant; + self.variants = variants; + } + + return self; +} + + +@end diff --git a/ios/Classes/CountlyiOS/CountlyExperimentInformation.h b/ios/Classes/CountlyiOS/CountlyExperimentInformation.h new file mode 100644 index 00000000..3f286f3d --- /dev/null +++ b/ios/Classes/CountlyiOS/CountlyExperimentInformation.h @@ -0,0 +1,23 @@ +// CountlyExperimentInfo.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +@interface CountlyExperimentInformation : NSObject + +@property (nonatomic, readonly) NSString* experimentID; +@property (nonatomic, readonly) NSString* experimentName; +@property (nonatomic, readonly) NSString* experimentDescription; +@property (nonatomic, readonly) NSString* currentVariant; +@property (nonatomic, readonly) NSDictionary* variants; + + +- (instancetype)initWithID:(NSString*)experimentID experimentName:(NSString*)experimentName experimentDescription:(NSString*)experimentDescription currentVariant:(NSString*)currentVariant variants:(NSDictionary*)variants; + +@end + + + diff --git a/ios/Classes/CountlyiOS/CountlyExperimentInformation.m b/ios/Classes/CountlyiOS/CountlyExperimentInformation.m new file mode 100644 index 00000000..bbaa6c61 --- /dev/null +++ b/ios/Classes/CountlyiOS/CountlyExperimentInformation.m @@ -0,0 +1,44 @@ +// CountlyExperimentInfo.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyExperimentInformation.h" + +@interface CountlyExperimentInformation () +@property (nonatomic) NSString* experimentID; +@property (nonatomic) NSString* experimentName; +@property (nonatomic) NSString* experimentDescription; +@property (nonatomic) NSString* currentVariant; +@property (nonatomic) NSDictionary* variants; +@end + + +@implementation CountlyExperimentInformation + +- (instancetype)init +{ + if (self = [super init]) + { + } + + return self; +} + +- (instancetype)initWithID:(NSString*)experimentID experimentName:(NSString*)experimentName experimentDescription:(NSString*)experimentDescription currentVariant:(NSString*)currentVariant variants:(NSDictionary*)variants +{ + if (self = [super init]) + { + self.experimentID = experimentID; + self.experimentName = experimentName; + self.experimentDescription = experimentDescription; + self.currentVariant = currentVariant; + self.variants = variants; + } + + return self; +} + + +@end diff --git a/ios/Classes/CountlyiOS/CountlyFeedbackWidget.h b/ios/Classes/CountlyiOS/CountlyFeedbackWidget.h index bcdf2ebf..73a91582 100644 --- a/ios/Classes/CountlyiOS/CountlyFeedbackWidget.h +++ b/ios/Classes/CountlyiOS/CountlyFeedbackWidget.h @@ -11,9 +11,11 @@ NS_ASSUME_NONNULL_BEGIN typedef NSString* CLYFeedbackWidgetType NS_EXTENSIBLE_STRING_ENUM; extern CLYFeedbackWidgetType const CLYFeedbackWidgetTypeSurvey; extern CLYFeedbackWidgetType const CLYFeedbackWidgetTypeNPS; +extern CLYFeedbackWidgetType const CLYFeedbackWidgetTypeRating; extern NSString* const kCountlyReservedEventSurvey; extern NSString* const kCountlyReservedEventNPS; +extern NSString* const kCountlyReservedEventRating; @interface CountlyFeedbackWidget : NSObject #if (TARGET_OS_IOS) @@ -21,6 +23,7 @@ extern NSString* const kCountlyReservedEventNPS; @property (nonatomic, readonly) CLYFeedbackWidgetType type; @property (nonatomic, readonly) NSString* ID; @property (nonatomic, readonly) NSString* name; +@property (nonatomic, readonly) NSArray* tags; @property (nonatomic, readonly) NSDictionary* data; /** diff --git a/ios/Classes/CountlyiOS/CountlyFeedbackWidget.m b/ios/Classes/CountlyiOS/CountlyFeedbackWidget.m index e40e6e57..e71e1d35 100644 --- a/ios/Classes/CountlyiOS/CountlyFeedbackWidget.m +++ b/ios/Classes/CountlyiOS/CountlyFeedbackWidget.m @@ -9,11 +9,13 @@ #import #endif -CLYFeedbackWidgetType const CLYFeedbackWidgetTypeSurvey = @"survey"; -CLYFeedbackWidgetType const CLYFeedbackWidgetTypeNPS = @"nps"; +CLYFeedbackWidgetType const CLYFeedbackWidgetTypeSurvey = @"survey"; +CLYFeedbackWidgetType const CLYFeedbackWidgetTypeNPS = @"nps"; +CLYFeedbackWidgetType const CLYFeedbackWidgetTypeRating = @"rating"; NSString* const kCountlyReservedEventSurvey = @"[CLY]_survey"; NSString* const kCountlyReservedEventNPS = @"[CLY]_nps"; +NSString* const kCountlyReservedEventRating = @"[CLY]_star_rating"; NSString* const kCountlyFBKeyClosed = @"closed"; NSString* const kCountlyFBKeyShown = @"shown"; @@ -22,6 +24,7 @@ @interface CountlyFeedbackWidget () @property (nonatomic) CLYFeedbackWidgetType type; @property (nonatomic) NSString* ID; @property (nonatomic) NSString* name; +@property (nonatomic) NSArray* tags; @property (nonatomic) NSDictionary* data; @end @@ -35,6 +38,7 @@ + (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary feedback.ID = dictionary[kCountlyFBKeyID]; feedback.type = dictionary[@"type"]; feedback.name = dictionary[@"name"]; + feedback.tags = dictionary[@"tg"]; return feedback; } @@ -209,6 +213,8 @@ - (void)recordReservedEventWithSegmentation:(NSDictionary *)segm eventName = kCountlyReservedEventSurvey; else if ([self.type isEqualToString:CLYFeedbackWidgetTypeNPS]) eventName = kCountlyReservedEventNPS; + else if ([self.type isEqualToString:CLYFeedbackWidgetTypeRating]) + eventName = kCountlyReservedEventRating; if (!eventName) { @@ -227,7 +233,7 @@ - (void)recordReservedEventWithSegmentation:(NSDictionary *)segm - (NSString *)description { - NSString *customDescription = [NSString stringWithFormat:@"\rID: %@, Type: %@ \rName: %@", self.ID, self.type, self.name]; + NSString *customDescription = [NSString stringWithFormat:@"\rID: %@, Type: %@ \rName: %@ \rTags: %@", self.ID, self.type, self.name, self.tags]; return [[super description] stringByAppendingString:customDescription]; } diff --git a/ios/Classes/CountlyiOS/CountlyRemoteConfig.h b/ios/Classes/CountlyiOS/CountlyRemoteConfig.h index b782913f..770e822b 100644 --- a/ios/Classes/CountlyiOS/CountlyRemoteConfig.h +++ b/ios/Classes/CountlyiOS/CountlyRemoteConfig.h @@ -6,6 +6,7 @@ #import #import "CountlyRCData.h" +#import "CountlyExperimentInformation.h" @interface CountlyRemoteConfig : NSObject @@ -38,6 +39,9 @@ - (void)testingEnrollIntoVariant:(NSString *)key variantName:(NSString *)variantName completionHandler:(RCVariantCallback)completionHandler; +- (void) testingDownloadExperimentInformation:(RCVariantCallback)completionHandler; +- (NSDictionary *) testingGetAllExperimentInfo; + - (void)clearAll; @end diff --git a/ios/Classes/CountlyiOS/CountlyRemoteConfig.m b/ios/Classes/CountlyiOS/CountlyRemoteConfig.m index c5a78eda..c57a7252 100644 --- a/ios/Classes/CountlyiOS/CountlyRemoteConfig.m +++ b/ios/Classes/CountlyiOS/CountlyRemoteConfig.m @@ -113,6 +113,19 @@ - (void)downloadOmittingKeys:(NSArray *)omitKeys completionHandler:(RCDownloadCa [CountlyRemoteConfigInternal.sharedInstance downloadValuesForKeys:nil omitKeys:omitKeys completionHandler:completionHandler]; } +- (void) testingDownloadExperimentInformation:(RCVariantCallback)completionHandler; +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); + + [CountlyRemoteConfigInternal.sharedInstance testingDownloadExperimentInformation:completionHandler]; +} + +- (NSDictionary *) testingGetAllExperimentInfo +{ + CLY_LOG_I(@"%s", __FUNCTION__); + return [CountlyRemoteConfigInternal.sharedInstance testingGetAllExperimentInfo]; +} + - (void)clearAll { [CountlyRemoteConfigInternal.sharedInstance clearAll]; diff --git a/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.h b/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.h index 4879761e..fe1622ad 100644 --- a/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.h +++ b/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.h @@ -32,6 +32,9 @@ - (void)testingDownloadAllVariants:(RCVariantCallback)completionHandler; - (void)testingEnrollIntoVariant:(NSString *)key variantName:(NSString *)variantName completionHandler:(RCVariantCallback)completionHandler; +- (void) testingDownloadExperimentInformation:(RCVariantCallback)completionHandler; +- (NSDictionary *) testingGetAllExperimentInfo; + - (NSDictionary *)getAllValues; - (void)enrollIntoABTestsForKeys:(NSArray *)keys; - (void)exitABTestsForKeys:(NSArray *)keys; diff --git a/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.m b/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.m index ec57c4cf..62e13f1b 100644 --- a/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.m +++ b/ios/Classes/CountlyiOS/CountlyRemoteConfigInternal.m @@ -9,6 +9,7 @@ NSString* const kCountlyRCKeyFetchRemoteConfig = @"fetch_remote_config"; NSString* const kCountlyRCKeyFetchVariant = @"ab_fetch_variants"; NSString* const kCountlyRCKeyEnrollVariant = @"ab_enroll_variant"; +NSString* const kCountlyRCKeyFetchExperiments = @"ab_fetch_experiments"; NSString* const kCountlyRCKeyVariant = @"variant"; NSString* const kCountlyRCKeyKey = @"key"; NSString* const kCountlyRCKeyKeys = @"keys"; @@ -27,6 +28,7 @@ @interface CountlyRemoteConfigInternal () @property (nonatomic) NSDictionary* localCachedVariants; @property (nonatomic) NSDictionary* cachedRemoteConfig; +@property (nonatomic) NSDictionary * localCachedExperiments; @end @implementation CountlyRemoteConfigInternal @@ -49,6 +51,8 @@ - (instancetype)init self.cachedRemoteConfig = [CountlyPersistency.sharedInstance retrieveRemoteConfig] ; self.remoteConfigGlobalCallbacks = [[NSMutableArray alloc] init]; + + self.localCachedExperiments = NSMutableDictionary.new; } return self; @@ -448,6 +452,7 @@ - (void)testingDownloadAllVariants:(RCVariantCallback)completionHandler }]; } + - (void)testingDownloadAllVariantsInternal:(void (^)(CLYRequestResult response, NSDictionary* variants, NSError * error))completionHandler { if (!CountlyServerConfig.sharedInstance.networkingEnabled) @@ -649,6 +654,139 @@ - (NSURLRequest *)downloadVariantsRequest } } +- (void) testingDownloadExperimentInformation:(RCVariantCallback)completionHandler +{ + if (!CountlyConsentManager.sharedInstance.consentForRemoteConfig) + { + CLY_LOG_D(@"'testingDownloadExperimentInformation' is aborted: RemoteConfig consent requires"); + return; + } + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_D(@"'testingDownloadExperimentInformation' is aborted: Due to temporary device id"); + return; + } + + CLY_LOG_D(@"Download experiments info..."); + + [self testingDownloaExperimentInfoInternal:^(CLYRequestResult response, NSDictionary *experimentInfo,NSError *error) + { + if (!error) + { + self.localCachedExperiments = experimentInfo; + CLY_LOG_D(@"Download experiments info is successful. \n%@", experimentInfo); + + } + else + { + CLY_LOG_W(@"Download experiments info failed: %@", error); + } + + if (completionHandler) + completionHandler(response, error); + }]; +} + + +- (void)testingDownloaExperimentInfoInternal:(void (^)(CLYRequestResult response, NSDictionary* experimentsInfo, NSError * error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'testingDownloaExperimentInfoInternal' is aborted: SDK Networking is disabled from server config!"); + return; + } + if (!completionHandler) + return; + + NSURLRequest* request = [self downloadExperimentInfoRequest]; + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + + NSMutableDictionary * experiments = NSMutableDictionary.new; + + if (!error) + { + + NSArray* experimentsInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + [experimentsInfo enumerateObjectsUsingBlock:^(NSDictionary* value, NSUInteger idx, BOOL * stop) + { + CountlyExperimentInformation* experimentInfo = [[CountlyExperimentInformation alloc] initWithID:value[@"id"] experimentName:value[@"name"] experimentDescription:value[@"description"] currentVariant:value[@"currentVariant"] variants:value[@"variants"]]; + experiments[experimentInfo.experimentID] = experimentInfo; + + }]; + } + + if (!error) + { + if (((NSHTTPURLResponse*)response).statusCode != 200) + { + NSMutableDictionary* userInfo = experiments.mutableCopy; + userInfo[NSLocalizedDescriptionKey] = @"Fetch variants general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorRemoteConfigGeneralAPIError userInfo:userInfo]; + } + } + + if (error) + { + CLY_LOG_D(@"Download experiments Request <%p> failed!\nError: %@", request, error); + + dispatch_async(dispatch_get_main_queue(), ^ + { + completionHandler(CLYResponseError, nil, error); + }); + + return; + } + + CLY_LOG_D(@"Download experiments Request <%p> successfully completed.", request); + + dispatch_async(dispatch_get_main_queue(), ^ + { + completionHandler(CLYResponseSuccess, experiments, nil); + }); + }]; + + [task resume]; + + CLY_LOG_D(@"Download experiments Request <%p> started:\n[%@] %@", (id)request, request.HTTPMethod, request.URL.absoluteString); +} + +- (NSURLRequest *)downloadExperimentInfoRequest +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyFetchExperiments]; + + if (CountlyConsentManager.sharedInstance.consentForSessions) + { + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMetrics, [CountlyDeviceInfo metrics]]; + } + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* serverOutputSDKEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@", + kCountlyEndpointO, + kCountlyEndpointSDK]; + + if (CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputSDKEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + NSString* withQueryString = [serverOutputSDKEndpoint stringByAppendingFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; + return request; + } +} +- (NSDictionary *) testingGetAllExperimentInfo +{ + return self.localCachedExperiments; +} + - (NSURLRequest *)enrollInVarianRequestForKey:(NSString *)key variantName:(NSString *)variantName { NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; diff --git a/lib/experiment_information.dart b/lib/experiment_information.dart new file mode 100644 index 00000000..849df3db --- /dev/null +++ b/lib/experiment_information.dart @@ -0,0 +1,25 @@ +class ExperimentInformation { + ExperimentInformation(this.experimentID, this.experimentName, this.experimentDescription, this.currentVariant, this.variants); + + final String experimentID; + final String experimentName; + final String experimentDescription; + final String currentVariant; + final Map> variants; + + static ExperimentInformation fromJson(dynamic json) { + Map> variantsMap = {}; + Map variants = json['variants'] ?? {}; + for (var item in variants.keys) + { + Map valueMap = {}; + Map values = variants[item] as Map; + for (var key in values.keys) + { + valueMap[key.toString()] = variants[key]; + } + variantsMap[item.toString()] = valueMap; + } + return ExperimentInformation(json['experimentID'] ?? "", json['experimentName'] ?? "", json['experimentDescription'] ?? "", json['currentVariant'] ?? "", variantsMap); + } +} \ No newline at end of file diff --git a/lib/remote_config.dart b/lib/remote_config.dart index 08fdf0a5..a9a7b368 100644 --- a/lib/remote_config.dart +++ b/lib/remote_config.dart @@ -1,3 +1,5 @@ +import 'package:countly_flutter/experiment_information.dart'; + /// REMOTE CONFIG / AB TESTING class RCData { Object? value; // stores the RC value @@ -54,4 +56,8 @@ abstract class RemoteConfig { Future testingDownloadVariantInformation(RCVariantCallback rcVariantCallback); Future testingEnrollIntoVariant(String keyName, String variantName, RCVariantCallback? rcVariantCallback); + + Future testingDownloadExperimentInformation(RCVariantCallback rcVariantCallback); + + Future> testingGetAllExperimentInfo(); } diff --git a/lib/remote_config_internal.dart b/lib/remote_config_internal.dart index 463e8a1a..758e5bd5 100644 --- a/lib/remote_config_internal.dart +++ b/lib/remote_config_internal.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:countly_flutter/countly_flutter.dart'; import 'package:countly_flutter/countly_state.dart'; +import 'package:countly_flutter/experiment_information.dart'; class RemoteConfigInternal implements RemoteConfig { RemoteConfigInternal(this._cly, this._countlyState); @@ -305,6 +306,34 @@ class RemoteConfigInternal implements RemoteConfig { return variant; } + Future testingDownloadExperimentInformation(RCVariantCallback rcVariantCallback) async { + if (!_countlyState.isInitialized) { + Countly.log('"initWithConfig" must be called before "testingDownloadExperimentInformation"', logLevel: LogLevel.ERROR); + return; + } + Countly.log('Calling "testingDownloadExperimentInformation"'); + int requestID = _wrapVariantCallback(rcVariantCallback); + + List args = []; + args.add(requestID); + + return await _countlyState.channel.invokeMethod('testingDownloadExperimentInformation', {'data': json.encode(args)}); + } + + Future> testingGetAllExperimentInfo() async + { + if (!_countlyState.isInitialized) { + Countly.log('"initWithConfig" must be called before "testingGetAllExperimentInfo"', logLevel: LogLevel.ERROR); + return {}; + } + + final List experimentsInfo = await _countlyState.channel.invokeMethod('testingGetAllExperimentInfo'); + List experimentsInfoList = experimentsInfo.map(ExperimentInformation.fromJson).toList(); + experimentsInfoList ??= []; + Map experimentsInfoMap = Map.fromIterable(experimentsInfoList, key: (e) => e.experimentID, value: (e) => e); + return experimentsInfoMap; + } + int _wrapDownloadCallback([RCDownloadCallback? callback]) { int requestID = _requestIDNoCallback; if (callback != null) { diff --git a/scripts/no-push-files/build.gradle b/scripts/no-push-files/build.gradle index 86e0073e..48c3cddf 100644 --- a/scripts/no-push-files/build.gradle +++ b/scripts/no-push-files/build.gradle @@ -34,5 +34,5 @@ android { } dependencies { - implementation 'ly.count.android:sdk:22.09.4' + implementation 'ly.count.android:sdk:23.8.1' }