From cf14ff59825cca0898b26615bcd7c30d0f7e6a31 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 24 Apr 2024 14:22:10 +0300 Subject: [PATCH 001/148] feat: visionos support --- Countly.h | 8 ++++---- Countly.m | 12 ++++++------ CountlyCommon.h | 2 +- CountlyCommon.m | 8 ++++---- CountlyConfig.h | 6 +++--- CountlyConfig.m | 2 +- CountlyConnectionManager.m | 2 +- CountlyConsentManager.m | 6 +++--- CountlyCrashReporter.m | 4 ++-- CountlyDeviceInfo.m | 35 +++++++++++++++++++--------------- CountlyNotificationService.h | 4 ++-- CountlyNotificationService.m | 2 +- CountlyPerformanceMonitoring.m | 4 ++-- CountlyPushNotifications.h | 2 +- CountlyPushNotifications.m | 12 ++++++------ CountlyViewTrackingInternal.h | 4 ++-- CountlyViewTrackingInternal.m | 32 +++++++++++++++---------------- 17 files changed, 75 insertions(+), 70 deletions(-) diff --git a/Countly.h b/Countly.h index f5659181..479d3e62 100644 --- a/Countly.h +++ b/Countly.h @@ -13,7 +13,7 @@ #import "CountlyFeedbackWidget.h" #import "CountlyViewTracking.h" #import "Resettable.h" -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) #import #endif @@ -366,7 +366,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Push Notification -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS /** * Shows default system dialog that asks for user's permission to display notifications. @@ -542,7 +542,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)recordView:(NSString *)viewName segmentation:(NSDictionary *)segmentation DEPRECATED_MSG_ATTRIBUTE("Use '[views startView/startAutoStoppedView:]' method instead!"); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) /** * Adds exception for AutoViewTracking. * @discussion @c UIViewControllers with specified title or class name will be ignored by AutoViewTracking and their appearances and disappearances will not be recorded. @@ -583,7 +583,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Feedbacks -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION ) /** * Shows star-rating dialog manually and executes completion block after user's action. * @discussion Completion block has a single NSInteger parameter that indicates 1 to 5 star-rating given by user. diff --git a/Countly.m b/Countly.m index f5f70233..64bb8a12 100644 --- a/Countly.m +++ b/Countly.m @@ -47,7 +47,7 @@ - (instancetype)init { if (self = [super init]) { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification @@ -192,7 +192,7 @@ - (void)startWithConfig:(CountlyConfig *)config [CountlyConnectionManager.sharedInstance sendAttribution]; } -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS if ([config.features containsObject:CLYPushNotifications]) { @@ -224,7 +224,7 @@ - (void)startWithConfig:(CountlyConfig *)config [CountlyCrashReporter.sharedInstance startCrashReporting]; } -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) { // Print deprecation flag for feature @@ -562,7 +562,7 @@ - (CLYDeviceIDType)deviceIDType if ([CountlyPersistency.sharedInstance retrieveIsCustomDeviceID]) return CLYDeviceIDTypeCustom; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) return CLYDeviceIDTypeIDFV; #else return CLYDeviceIDTypeNSUUID; @@ -926,7 +926,7 @@ - (void)cancelEvent:(NSString *)key #pragma mark - Push Notifications -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - (void)askForNotificationPermission @@ -1079,7 +1079,7 @@ - (void)recordView:(NSString *)viewName segmentation:(NSDictionary *)segmentatio [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; } -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - (void)addExceptionForAutoViewTracking:(NSString *)exception { CLY_LOG_I(@"%s %@", __FUNCTION__, exception); diff --git a/CountlyCommon.h b/CountlyCommon.h index 36a8562b..5c416a0f 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -104,7 +104,7 @@ void CountlyPrint(NSString *stringToPrint); - (void)startBackgroundTask; - (void)finishBackgroundTask; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - (UIViewController *)topViewController; - (void)tryPresentingViewController:(UIViewController *)viewController; - (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion; diff --git a/CountlyCommon.m b/CountlyCommon.m index 75c9ddd2..f00cc0a7 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -17,11 +17,11 @@ @interface CountlyCommon () } @property long long lastTimestamp; -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION ) @property (nonatomic) NSString* lastInterfaceOrientation; #endif -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) @property (nonatomic) UIBackgroundTaskIdentifier bgTask; #endif @end @@ -231,7 +231,7 @@ - (void)recordOrientation - (void)startBackgroundTask { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.bgTask != UIBackgroundTaskInvalid) return; @@ -245,7 +245,7 @@ - (void)startBackgroundTask - (void)finishBackgroundTask { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.bgTask != UIBackgroundTaskInvalid && !CountlyConnectionManager.sharedInstance.connection) { [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; diff --git a/CountlyConfig.h b/CountlyConfig.h index bf4bc9bf..b6c13095 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -10,7 +10,7 @@ #import "CountlyAPMConfig.h" #import "CountlySDKLimitsConfig.h" -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) #import #endif @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN //NOTE: Countly features typedef NSString* CLYFeature NS_EXTENSIBLE_STRING_ENUM; -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION ) #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS extern CLYFeature const CLYPushNotifications; #endif @@ -213,7 +213,7 @@ typedef enum : NSUInteger #pragma mark - -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) /** * For enabling automatic view tacking. * @discussion If set, views will automatically track. diff --git a/CountlyConfig.m b/CountlyConfig.m index f93150e5..572d6894 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -17,7 +17,7 @@ - (void)enableAPMInternal:(BOOL)enableAPM; @implementation CountlyConfig //NOTE: Countly features -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) CLYFeature const CLYPushNotifications = @"CLYPushNotifications"; CLYFeature const CLYCrashReporting = @"CLYCrashReporting"; // CLYAutoViewTracking is deprecated, Use 'config.enableAutomaticViewTracking' instead diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index e389c635..1a2b674b 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -775,7 +775,7 @@ - (NSString *)attributionQueryString - (NSMutableData *)pictureUploadDataForQueryString:(NSString *)queryString { -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) NSString* localPicturePath = nil; NSString* userDetails = [queryString cly_valueForQueryStringKey:kCountlyQSKeyUserDetails]; diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index fce1c291..62d3a278 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -311,7 +311,7 @@ - (void)setConsentForPushNotifications:(BOOL)consentForPushNotifications { _consentForPushNotifications = consentForPushNotifications; -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) if (consentForPushNotifications) { CLY_LOG_D(@"Consent for PushNotifications is given."); @@ -352,7 +352,7 @@ - (void)setConsentForViewTracking:(BOOL)consentForViewTracking { _consentForViewTracking = consentForViewTracking; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (consentForViewTracking) { CLY_LOG_D(@"Consent for ViewTracking is given."); @@ -390,7 +390,7 @@ - (void)setConsentForPerformanceMonitoring:(BOOL)consentForPerformanceMonitoring { _consentForPerformanceMonitoring = consentForPerformanceMonitoring; -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) if (consentForPerformanceMonitoring) { CLY_LOG_D(@"Consent for PerformanceMonitoring is given."); diff --git a/CountlyCrashReporter.m b/CountlyCrashReporter.m index f3c6e30c..7b16237d 100644 --- a/CountlyCrashReporter.m +++ b/CountlyCrashReporter.m @@ -107,7 +107,7 @@ - (void)startCrashReporting NSSetUncaughtExceptionHandler(&CountlyUncaughtExceptionHandler); -#if (TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV || TARGET_OS_OSX) signal(SIGABRT, CountlySignalHandler); signal(SIGILL, CountlySignalHandler); signal(SIGSEGV, CountlySignalHandler); @@ -126,7 +126,7 @@ - (void)stopCrashReporting NSSetUncaughtExceptionHandler(NULL); -#if (TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV || TARGET_OS_OSX) signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); diff --git a/CountlyDeviceInfo.m b/CountlyDeviceInfo.m index 41c55321..8c140388 100644 --- a/CountlyDeviceInfo.m +++ b/CountlyDeviceInfo.m @@ -65,7 +65,7 @@ - (instancetype)init #endif #endif -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) self.isInBackground = (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground); [NSNotificationCenter.defaultCenter addObserver:self @@ -107,7 +107,7 @@ - (NSString *)ensafeDeviceID:(NSString *)deviceID if (deviceID.length) return deviceID; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) return UIDevice.currentDevice.identifierForVendor.UUIDString; #else NSString* UUID = [CountlyPersistency.sharedInstance retrieveNSUUID]; @@ -182,6 +182,8 @@ + (NSString *)deviceType return @"smarttv"; #elif (TARGET_OS_OSX) return @"desktop"; +#elif (TARGET_OS_VISION) + return @"vr"; #endif return nil; @@ -218,6 +220,8 @@ + (NSString *)osName return @"tvOS"; #elif (TARGET_OS_OSX) return @"macOS"; +#elif (TARGET_OS_VISION) + return @"visionOS"; #endif return nil; @@ -225,7 +229,7 @@ + (NSString *)osName + (NSString *)osVersion { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) return UIDevice.currentDevice.systemVersion; #elif (TARGET_OS_WATCH) return WKInterfaceDevice.currentDevice.systemVersion; @@ -260,34 +264,35 @@ + (NSString *)carrier + (NSString *)resolution { + CGRect bounds; + CGFloat scale; #if (TARGET_OS_IOS || TARGET_OS_TV) - CGRect bounds = UIScreen.mainScreen.bounds; - CGFloat scale = UIScreen.mainScreen.scale; + bounds = UIScreen.mainScreen.bounds; + scale = UIScreen.mainScreen.scale; #elif (TARGET_OS_WATCH) - CGRect bounds = WKInterfaceDevice.currentDevice.screenBounds; - CGFloat scale = WKInterfaceDevice.currentDevice.screenScale; + bounds = WKInterfaceDevice.currentDevice.screenBounds; + scale = WKInterfaceDevice.currentDevice.screenScale; #elif (TARGET_OS_OSX) - NSRect bounds = NSScreen.mainScreen.frame; - CGFloat scale = NSScreen.mainScreen.backingScaleFactor; + bounds = NSScreen.mainScreen.frame; + scale = NSScreen.mainScreen.backingScaleFactor; #else return nil; #endif - return [NSString stringWithFormat:@"%gx%g", bounds.size.width * scale, bounds.size.height * scale]; } + (NSString *)density { + CGFloat scale; #if (TARGET_OS_IOS || TARGET_OS_TV) - CGFloat scale = UIScreen.mainScreen.scale; + scale = UIScreen.mainScreen.scale; #elif (TARGET_OS_WATCH) - CGFloat scale = WKInterfaceDevice.currentDevice.screenScale; + scale = WKInterfaceDevice.currentDevice.screenScale; #elif (TARGET_OS_OSX) - CGFloat scale = NSScreen.mainScreen.backingScaleFactor; + scale = NSScreen.mainScreen.backingScaleFactor; #else return nil; #endif - return [NSString stringWithFormat:@"@%dx", (int)scale]; } @@ -409,7 +414,7 @@ + (unsigned long long)totalDisk // If it is not possible to retrieve a valid value then it will return a -1. + (NSInteger)batteryLevel { -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) // If battey state is "unknown" that means that battery monitoring is not enabled. // In that case we will not able to retrieve a battery level. if (UIDevice.currentDevice.batteryState == UIDeviceBatteryStateUnknown) diff --git a/CountlyNotificationService.h b/CountlyNotificationService.h index 9d9c585e..66e5ec5c 100644 --- a/CountlyNotificationService.h +++ b/CountlyNotificationService.h @@ -6,7 +6,7 @@ #import -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) #import #endif @@ -24,7 +24,7 @@ extern NSString* const kCountlyPNKeyActionButtonTitle; extern NSString* const kCountlyPNKeyActionButtonURL; @interface CountlyNotificationService : NSObject -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) + (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *))contentHandler API_AVAILABLE(ios(10.0)); #endif diff --git a/CountlyNotificationService.m b/CountlyNotificationService.m index 8ebf8fda..a42a08ad 100644 --- a/CountlyNotificationService.m +++ b/CountlyNotificationService.m @@ -25,7 +25,7 @@ NSString* const kCountlyPNKeyActionButtonURL = @"l"; @implementation CountlyNotificationService -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) + (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *))contentHandler { COUNTLY_EXT_LOG(@"didReceiveNotificationRequest:withContentHandler:"); diff --git a/CountlyPerformanceMonitoring.m b/CountlyPerformanceMonitoring.m index 35fbbf26..00cdf1ea 100644 --- a/CountlyPerformanceMonitoring.m +++ b/CountlyPerformanceMonitoring.m @@ -84,7 +84,7 @@ - (void)startPerformanceMonitoring #if (TARGET_OS_OSX) [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillResignActive:) name:NSApplicationWillResignActiveNotification object:nil]; -#elif (TARGET_OS_IOS || TARGET_OS_TV) +#elif (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; #endif @@ -100,7 +100,7 @@ - (void)stopPerformanceMonitoring #if (TARGET_OS_OSX) [NSNotificationCenter.defaultCenter removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; [NSNotificationCenter.defaultCenter removeObserver:self name:NSApplicationWillResignActiveNotification object:nil]; -#elif (TARGET_OS_IOS || TARGET_OS_TV) +#elif (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; #endif diff --git a/CountlyPushNotifications.h b/CountlyPushNotifications.h index 07293a74..dd92bd0d 100644 --- a/CountlyPushNotifications.h +++ b/CountlyPushNotifications.h @@ -18,7 +18,7 @@ extern NSString* const kCountlyReservedEventPushAction; + (instancetype)sharedInstance; -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) - (void)startPushNotifications; - (void)stopPushNotifications; - (void)askForNotificationPermissionWithOptions:(NSUInteger)options completionHandler:(void (^)(BOOL granted, NSError * error))completionHandler; diff --git a/CountlyPushNotifications.m b/CountlyPushNotifications.m index 1e4fbbe6..4b2cc319 100644 --- a/CountlyPushNotifications.m +++ b/CountlyPushNotifications.m @@ -17,7 +17,7 @@ CLYPushTestMode const CLYPushTestModeDevelopment = @"CLYPushTestModeDevelopment"; CLYPushTestMode const CLYPushTestModeTestFlightOrAdHoc = @"CLYPushTestModeTestFlightOrAdHoc"; -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) @interface CountlyPushNotifications () @property (nonatomic) NSString* token; #else @@ -25,7 +25,7 @@ @interface CountlyPushNotifications () #endif @end -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) #define CLYApplication UIApplication #elif (TARGET_OS_OSX) #define CLYApplication NSApplication @@ -58,7 +58,7 @@ - (instancetype)init #pragma mark --- -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) - (void)startPushNotifications { if (!self.isEnabledOnInitialConfig) @@ -198,7 +198,7 @@ - (void)openURL:(NSString *)URLString dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) [UIApplication.sharedApplication openURL:[NSURL URLWithString:URLString] options:@{} completionHandler:nil]; #elif (TARGET_OS_OSX) [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:URLString]]; @@ -223,7 +223,7 @@ - (void)recordActionEvent:(NSString *)notificationID buttonIndex:(NSInteger)butt return; NSString* platform = @"unknown"; -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) platform = kCountlyPNKeyiOS; #elif (TARGET_OS_OSX) platform = kCountlyPNKeymacOS; @@ -338,7 +338,7 @@ - (void)application:(CLYApplication *)application didFailToRegisterForRemoteNoti @implementation NSObject (CountlyPushNotifications) -#if (TARGET_OS_IOS || TARGET_OS_OSX) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) - (void)Countly_application:(CLYApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { CLY_LOG_D(@"App didRegisterForRemoteNotificationsWithDeviceToken: %@", deviceToken); diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index a27e19c8..a3601875 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -15,7 +15,7 @@ extern NSString* const kCountlyReservedEventView; + (instancetype)sharedInstance; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (void)startAutoViewTracking; - (void)stopAutoViewTracking; - (void)addExceptionForAutoViewTracking:(NSString *)exception; @@ -41,7 +41,7 @@ extern NSString* const kCountlyReservedEventView; - (void)addSegmentationToViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentation; - (void)addSegmentationToViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segmentation; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (void)addAutoViewTrackingExclutionList:(NSArray *)viewTrackingExclusionList; #endif @end diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index e36ffcb1..13203d36 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -7,7 +7,7 @@ #import "CountlyCommon.h" @interface CountlyViewTrackingInternal () -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) @property (nonatomic) NSMutableSet* automaticViewTrackingExclusionList; #endif @property (nonatomic) NSMutableDictionary * viewDataDictionary; @@ -27,7 +27,7 @@ @interface CountlyViewTrackingInternal () NSString* const kCountlyVTKeyDomain = @"domain"; NSString* const kCountlyVTKeyDur = @"dur"; -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) @interface UIViewController (CountlyViewTracking) - (void)Countly_viewDidAppear:(BOOL)animated; - (void)Countly_viewDidDisappear:(BOOL)animated; @@ -51,7 +51,7 @@ - (instancetype)init { if (self = [super init]) { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) self.automaticViewTrackingExclusionList = @[ @"CLYInternalViewController", @@ -148,7 +148,7 @@ - (void)updateGlobalViewSegmentation:(NSDictionary *)segmentation - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return nil; @@ -161,7 +161,7 @@ - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmen - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return nil; @@ -174,7 +174,7 @@ - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictiona - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -187,7 +187,7 @@ - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segme - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewID, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -199,7 +199,7 @@ - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentat - (void)pauseViewWithID:(NSString *)viewID { CLY_LOG_I(@"%s %@", __FUNCTION__, viewID); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually pause view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -211,7 +211,7 @@ - (void)pauseViewWithID:(NSString *)viewID - (void)resumeViewWithID:(NSString *)viewID { CLY_LOG_I(@"%s %@", __FUNCTION__, viewID); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually resume view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -223,7 +223,7 @@ - (void)resumeViewWithID:(NSString *)viewID - (void)stopAllViews:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@", __FUNCTION__, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -233,7 +233,7 @@ - (void)stopAllViews:(NSDictionary *)segmentation } -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (void)addAutoViewTrackingExclutionList:(NSArray *)viewTrackingExclusionList { [self.automaticViewTrackingExclusionList addObjectsFromArray:viewTrackingExclusionList]; @@ -619,7 +619,7 @@ - (void)addSegmentationToViewWithName:(NSString *)viewName segmentation:(NSDicti #pragma mark - Internal auto view tracking methods -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (void)swizzleViewTrackingMethods { @@ -703,7 +703,7 @@ - (NSString*)titleForViewController:(UIViewController *)viewController #pragma mark - Public function for application state - (void)applicationWillEnterForeground { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { } @@ -715,7 +715,7 @@ - (void)applicationWillEnterForeground { #endif } - (void)applicationDidEnterBackground { -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { [self stopCurrentView]; } @@ -742,7 +742,7 @@ - (void)resetFirstView #pragma mark - -#if (TARGET_OS_IOS || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) @implementation UIViewController (CountlyViewTracking) - (void)Countly_viewDidAppear:(BOOL)animated { @@ -784,7 +784,7 @@ - (BOOL)isPageSheetModal { //NOTE: iOS 13 check is not related to availability of UIModalPresentationPageSheet, // but needed due to behavioral difference in presenting logic compared to previous iOS versions. -#if (TARGET_OS_IOS) +#if (TARGET_OS_IOS || TARGET_OS_VISION) if (@available(iOS 13.0, *)) { if (self.modalPresentationStyle == UIModalPresentationPageSheet && self.isBeingPresented) From 83e02cbfcb2dd3e256a79099dc0701a97ffd3aeb Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 24 Apr 2024 14:33:03 +0300 Subject: [PATCH 002/148] fix: remove auto view tracking support --- Countly.m | 2 +- CountlyViewTrackingInternal.m | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Countly.m b/Countly.m index 64bb8a12..73cc266e 100644 --- a/Countly.m +++ b/Countly.m @@ -224,7 +224,7 @@ - (void)startWithConfig:(CountlyConfig *)config [CountlyCrashReporter.sharedInstance startCrashReporting]; } -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) +#if (TARGET_OS_IOS || TARGET_OS_TV ) if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) { // Print deprecation flag for feature diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 13203d36..e85c5f8d 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -7,7 +7,7 @@ #import "CountlyCommon.h" @interface CountlyViewTrackingInternal () -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) @property (nonatomic) NSMutableSet* automaticViewTrackingExclusionList; #endif @property (nonatomic) NSMutableDictionary * viewDataDictionary; @@ -27,7 +27,7 @@ @interface CountlyViewTrackingInternal () NSString* const kCountlyVTKeyDomain = @"domain"; NSString* const kCountlyVTKeyDur = @"dur"; -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) @interface UIViewController (CountlyViewTracking) - (void)Countly_viewDidAppear:(BOOL)animated; - (void)Countly_viewDidDisappear:(BOOL)animated; @@ -51,7 +51,7 @@ - (instancetype)init { if (self = [super init]) { -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) self.automaticViewTrackingExclusionList = @[ @"CLYInternalViewController", @@ -148,7 +148,7 @@ - (void)updateGlobalViewSegmentation:(NSDictionary *)segmentation - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return nil; @@ -161,7 +161,7 @@ - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmen - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return nil; @@ -174,7 +174,7 @@ - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictiona - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -187,7 +187,7 @@ - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segme - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewID, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -199,7 +199,7 @@ - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentat - (void)pauseViewWithID:(NSString *)viewID { CLY_LOG_I(@"%s %@", __FUNCTION__, viewID); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually pause view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -211,7 +211,7 @@ - (void)pauseViewWithID:(NSString *)viewID - (void)resumeViewWithID:(NSString *)viewID { CLY_LOG_I(@"%s %@", __FUNCTION__, viewID); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually resume view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -223,7 +223,7 @@ - (void)resumeViewWithID:(NSString *)viewID - (void)stopAllViews:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@", __FUNCTION__, segmentation); -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); return; @@ -233,7 +233,7 @@ - (void)stopAllViews:(NSDictionary *)segmentation } -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) - (void)addAutoViewTrackingExclutionList:(NSArray *)viewTrackingExclusionList { [self.automaticViewTrackingExclusionList addObjectsFromArray:viewTrackingExclusionList]; @@ -619,7 +619,7 @@ - (void)addSegmentationToViewWithName:(NSString *)viewName segmentation:(NSDicti #pragma mark - Internal auto view tracking methods -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) - (void)swizzleViewTrackingMethods { @@ -742,7 +742,7 @@ - (void)resetFirstView #pragma mark - -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_TV) @implementation UIViewController (CountlyViewTracking) - (void)Countly_viewDidAppear:(BOOL)animated { @@ -784,7 +784,7 @@ - (BOOL)isPageSheetModal { //NOTE: iOS 13 check is not related to availability of UIModalPresentationPageSheet, // but needed due to behavioral difference in presenting logic compared to previous iOS versions. -#if (TARGET_OS_IOS || TARGET_OS_VISION) +#if (TARGET_OS_IOS) if (@available(iOS 13.0, *)) { if (self.modalPresentationStyle == UIModalPresentationPageSheet && self.isBeingPresented) From 60173b58538627c0165550cf04e3a2d287f5bf7c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 8 Jul 2024 16:28:06 +0500 Subject: [PATCH 003/148] Added 'enableTemporaryIDMode' method for config and after init --- Countly.h | 9 ++++++++- Countly.m | 5 +++++ CountlyConfig.h | 7 +++++++ CountlyConfig.m | 6 +++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Countly.h b/Countly.h index 43ff3981..81a0ab21 100644 --- a/Countly.h +++ b/Countly.h @@ -186,7 +186,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)changeDeviceIDWithoutMerge:(NSString * _Nullable)deviceID; -- (void) setID:(NSString *)deviceID; +- (void)setID:(NSString *)deviceID; + +/** + * This menthod will enable temporary device ID mode + * @discussion All requests will be on hold, but they will be persistently stored. + * @discussion When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored. + */ +- (void)enableTemporaryIDMode; #pragma mark - Consents diff --git a/Countly.m b/Countly.m index d03d7691..c6e6bd84 100644 --- a/Countly.m +++ b/Countly.m @@ -604,6 +604,11 @@ - (void)changeDeviceIDWithoutMerge:(NSString * _Nullable)deviceID { [self setIDInternal:deviceID onServer:NO]; } +- (void)enableTemporaryIDMode +{ + [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; +} + - (void)setNewDeviceID:(NSString *)deviceID onServer:(BOOL)onServer { [Countly.sharedInstance setIDInternal:deviceID onServer:onServer]; diff --git a/CountlyConfig.h b/CountlyConfig.h index 1fcac7e3..a6daf500 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -335,6 +335,13 @@ typedef enum : NSUInteger */ @property (nonatomic, copy) NSString* deviceID; +/** + * This menthod will enable temporary device ID mode + * @discussion All requests will be on hold, but they will be persistently stored. + * @discussion When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored. + */ +- (void)enableTemporaryIDMode; + /** * For resetting persistently stored device ID on SDK start. * @discussion If set, persistently stored device ID will be reset and new device ID specified on @c deviceID property of @c CountlyConfig object will be stored and used. diff --git a/CountlyConfig.m b/CountlyConfig.m index b3322aa9..9fa6eaa9 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -78,13 +78,17 @@ - (instancetype)init return self; } +- (void)enableTemporaryIDMode +{ + self.deviceID = CLYTemporaryDeviceID; +} + -(void)remoteConfigRegisterGlobalCallback:(RCDownloadCallback) callback { [self.remoteConfigGlobalCallbacks addObject:callback]; } - - (NSMutableArray *) getRemoteConfigGlobalCallbacks { return self.remoteConfigGlobalCallbacks; From 02f90401e38ec7c5d842e215cdc90530adb17d16 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 8 Jul 2024 16:56:38 +0500 Subject: [PATCH 004/148] Added changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aff6f457..dff8098e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 24.7.1 +* Added `enableTemporaryIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. + ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method * Added `setID` method for changing device ID based on the device ID type From 1d62f8b88f962a0556d1a7a6fed6a6f9976fbf09 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 8 Jul 2024 16:58:42 +0500 Subject: [PATCH 005/148] Updated SDK version to 24.7.1 --- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 868150d2..9cf63e24 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.0' + s.version = '24.7.1' 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' diff --git a/Countly.podspec b/Countly.podspec index d25c0d23..9b5499ae 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.0' + s.version = '24.7.1' 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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 884e8394..f1707279 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -674,7 +674,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.0; + MARKETING_VERSION = 24.7.1; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -706,7 +706,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.0; + MARKETING_VERSION = 24.7.1; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index 15222763..13d98141 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -26,7 +26,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.0"; +NSString* const kCountlySDKVersion = @"24.7.1"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; From 3450b44917b43bceba0eb481cb109b57dc5a4277 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 9 Jul 2024 17:52:42 +0500 Subject: [PATCH 006/148] Updated method names --- CHANGELOG.md | 2 +- Countly.h | 2 +- Countly.m | 2 +- CountlyConfig.h | 2 +- CountlyConfig.m | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff8098e..53435fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## 24.7.1 -* Added `enableTemporaryIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. +* Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method diff --git a/Countly.h b/Countly.h index 81a0ab21..de5c8241 100644 --- a/Countly.h +++ b/Countly.h @@ -193,7 +193,7 @@ NS_ASSUME_NONNULL_BEGIN * @discussion All requests will be on hold, but they will be persistently stored. * @discussion When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored. */ -- (void)enableTemporaryIDMode; +- (void)enableTemporaryDeviceIDMode; #pragma mark - Consents diff --git a/Countly.m b/Countly.m index c6e6bd84..1c5c40a8 100644 --- a/Countly.m +++ b/Countly.m @@ -604,7 +604,7 @@ - (void)changeDeviceIDWithoutMerge:(NSString * _Nullable)deviceID { [self setIDInternal:deviceID onServer:NO]; } -- (void)enableTemporaryIDMode +- (void)enableTemporaryDeviceIDMode { [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; } diff --git a/CountlyConfig.h b/CountlyConfig.h index a6daf500..fbc2e979 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -340,7 +340,7 @@ typedef enum : NSUInteger * @discussion All requests will be on hold, but they will be persistently stored. * @discussion When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored. */ -- (void)enableTemporaryIDMode; +- (void)enableTemporaryDeviceIDMode; /** * For resetting persistently stored device ID on SDK start. diff --git a/CountlyConfig.m b/CountlyConfig.m index 9fa6eaa9..5956f790 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -78,7 +78,7 @@ - (instancetype)init return self; } -- (void)enableTemporaryIDMode +- (void)enableTemporaryDeviceIDMode { self.deviceID = CLYTemporaryDeviceID; } From d1cb8e8b5ff366f131058166bf4e7d1dbe75019d Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 18 Jul 2024 12:59:41 +0500 Subject: [PATCH 007/148] Preventing SDK to automatics start session in background --- CHANGELOG.md | 2 ++ Countly.m | 31 +++++++++++++++++++++++++++++-- CountlyConnectionManager.m | 5 +++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53435fcc..a4a51605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## 24.7.1 * Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. +* Fixed an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent. + ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method diff --git a/Countly.m b/Countly.m index 1c5c40a8..220af5d9 100644 --- a/Countly.m +++ b/Countly.m @@ -60,11 +60,25 @@ - (instancetype)init selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification + object:nil]; #elif (TARGET_OS_OSX) [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillResignActive:) name:NSApplicationWillResignActiveNotification + object:nil]; #endif } @@ -404,16 +418,28 @@ - (void)resume isSuspended = NO; } +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + CLY_LOG_D(@"App enters foreground"); + [self resume]; +} + +- (void)applicationWillResignActive:(NSNotification *)notification +{ + CLY_LOG_D(@"App enters background"); + [self suspend]; +} + - (void)applicationDidEnterBackground:(NSNotification *)notification { CLY_LOG_D(@"App did enter background."); - [self suspend]; +// [self suspend]; } - (void)applicationWillEnterForeground:(NSNotification *)notification { CLY_LOG_D(@"App will enter foreground."); - [self resume]; +// [self resume]; } - (void)applicationWillTerminate:(NSNotification *)notification @@ -431,6 +457,7 @@ - (void)applicationWillTerminate:(NSNotification *)notification [CountlyPersistency.sharedInstance saveToFileSync]; } + - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index f9cbb89c..981ce2da 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -354,6 +354,11 @@ - (void)beginSession CLY_LOG_W(@"%s A session is already running, this 'beginSession' will be ignored", __FUNCTION__); return; } + + if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, session will not be started", __FUNCTION__); + return; + } isSessionStarted = YES; lastSessionStartTime = NSDate.date.timeIntervalSince1970; From df9493892f6cdd3a82942b9f5d506137cf6e8a51 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 18 Jul 2024 15:21:26 +0500 Subject: [PATCH 008/148] Added a background check in update session --- CountlyConnectionManager.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 981ce2da..b3f9db1b 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -356,7 +356,7 @@ - (void)beginSession } if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - CLY_LOG_W(@"%s App is in the background, session will not be started", __FUNCTION__); + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); return; } @@ -390,6 +390,11 @@ - (void)updateSession CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); return; } + + if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'updateSession' will be ignored", __FUNCTION__); + return; + } NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; From 25a52ec32cfa65badcf4a1c169ca48260e4efaa4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 18 Jul 2024 15:25:24 +0500 Subject: [PATCH 009/148] reverted suspend call changes --- Countly.m | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Countly.m b/Countly.m index 220af5d9..def8eece 100644 --- a/Countly.m +++ b/Countly.m @@ -427,19 +427,17 @@ - (void)applicationDidBecomeActive:(NSNotification *)notification - (void)applicationWillResignActive:(NSNotification *)notification { CLY_LOG_D(@"App enters background"); - [self suspend]; } - (void)applicationDidEnterBackground:(NSNotification *)notification { CLY_LOG_D(@"App did enter background."); -// [self suspend]; + [self suspend]; } - (void)applicationWillEnterForeground:(NSNotification *)notification { CLY_LOG_D(@"App will enter foreground."); -// [self resume]; } - (void)applicationWillTerminate:(NSNotification *)notification From 42070b418868fc5cdecaf5823890163c344fd34b Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 18 Jul 2024 18:27:48 +0500 Subject: [PATCH 010/148] Removed background check for update session --- CountlyConnectionManager.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index b3f9db1b..5d80367c 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -390,11 +390,6 @@ - (void)updateSession CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); return; } - - if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - CLY_LOG_W(@"%s App is in the background, 'updateSession' will be ignored", __FUNCTION__); - return; - } NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; From 2c1da86e19e8c1673cecb0afa0a2e6c354a0547c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 18 Jul 2024 19:00:15 +0500 Subject: [PATCH 011/148] send consent if require consents is true even if no consent is given --- CHANGELOG.md | 1 + Countly.m | 2 ++ CountlyConsentManager.h | 1 + 3 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53435fcc..af4386da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 24.7.1 * Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. +* Fixed an issue where consent was not sent when requires consent was true, but no consent was given ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method diff --git a/Countly.m b/Countly.m index 1c5c40a8..42c88278 100644 --- a/Countly.m +++ b/Countly.m @@ -272,6 +272,8 @@ - (void)startWithConfig:(CountlyConfig *)config [self giveAllConsents]; else if (config.consents) [self giveConsentForFeatures:config.consents]; + else if (config.requiresConsent) + [CountlyConsentManager.sharedInstance sendConsents]; if (config.campaignType && config.campaignData) [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; diff --git a/CountlyConsentManager.h b/CountlyConsentManager.h index 4c8d231a..5846a636 100644 --- a/CountlyConsentManager.h +++ b/CountlyConsentManager.h @@ -30,5 +30,6 @@ - (void)cancelConsentForAllFeatures; - (void)cancelConsentForAllFeaturesWithoutSendingConsentsRequest; - (BOOL)hasAnyConsent; +- (void)sendConsents; @end From 501fadda6513a921950d80fccd1ccf005fbe4748 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 22 Jul 2024 15:25:38 +0500 Subject: [PATCH 012/148] Send an empty location if location is disabled or location consent is not given, without checking for location consent. --- Countly.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Countly.m b/Countly.m index 1c5c40a8..0b3620f1 100644 --- a/Countly.m +++ b/Countly.m @@ -172,6 +172,7 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyFeedbacks.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; CountlyFeedbacks.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; +#endif if(config.disableLocation) { @@ -181,7 +182,6 @@ - (void)startWithConfig:(CountlyConfig *)config { [CountlyLocationManager.sharedInstance updateLocation:config.location city:config.city ISOCountryCode:config.ISOCountryCode IP:config.IP]; } -#endif if (!CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance beginSession]; @@ -189,7 +189,15 @@ - (void)startWithConfig:(CountlyConfig *)config //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. if (!CountlyConsentManager.sharedInstance.consentForSessions) { - [CountlyLocationManager.sharedInstance sendLocationInfo]; + //Send an empty location if location is disabled or location consent is not given, without checking for location consent. + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } + else + { + [CountlyLocationManager.sharedInstance sendLocationInfo]; + } [CountlyConnectionManager.sharedInstance sendAttribution]; } From 8151e3867436f9f7b1ac6edc36dd4326799256ab Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 22 Jul 2024 18:26:56 +0500 Subject: [PATCH 013/148] Sending empty location when consent is removed for location --- CountlyConsentManager.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 9e5bd459..3cd62f79 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -350,6 +350,8 @@ - (void)setConsentForLocation:(BOOL)consentForLocation else { CLY_LOG_D(@"Consent for Location is cancelled."); + + [CountlyConnectionManager.sharedInstance sendLocationInfo]; } } From b2ab2ad435b5ddbe62a4b9b3a9b188895f327a6c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 22 Jul 2024 22:34:08 +0500 Subject: [PATCH 014/148] Send orientation during initialization and consent given for user details --- Countly.m | 2 ++ CountlyCommon.h | 2 ++ CountlyConsentManager.m | 1 + 3 files changed, 5 insertions(+) diff --git a/Countly.m b/Countly.m index 1c5c40a8..a23e585b 100644 --- a/Countly.m +++ b/Countly.m @@ -186,6 +186,8 @@ - (void)startWithConfig:(CountlyConfig *)config if (!CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance beginSession]; + [CountlyCommon.sharedInstance recordOrientation]; + //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. if (!CountlyConsentManager.sharedInstance.consentForSessions) { diff --git a/CountlyCommon.h b/CountlyCommon.h index 5019add2..9eff5880 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -113,6 +113,8 @@ void CountlyPrint(NSString *stringToPrint); - (void)observeDeviceOrientationChanges; +- (void)recordOrientation; + - (BOOL)hasStarted_; @end diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 9e5bd459..ebf015c1 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -283,6 +283,7 @@ - (void)setConsentForUserDetails:(BOOL)consentForUserDetails if (consentForUserDetails) { CLY_LOG_D(@"Consent for UserDetails is given."); + [CountlyCommon.sharedInstance recordOrientation]; [Countly.user save]; } else From c04ed355c8c6cb16aeb4a551697e346c91311ae4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 25 Jul 2024 15:19:21 +0500 Subject: [PATCH 015/148] Updated changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ffbc6d..ba7d2bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ ## 24.7.1 * Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. +* Orientation is now also sent during initialization and when consent is given. +* Mitigated an issue where consent was not sent when requires consent was true, but no consent was given +* Mitigated an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent. +* Mitigated an issue where empty location was not being sent when location is disabled or location consent is not given -* Fixed an issue where consent was not sent when requires consent was true, but no consent was given -* Fixed an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent. ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method From 11a2e5a0f3628011f94eb0ee2b3852c2a899fd0b Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 29 Jul 2024 11:17:29 +0500 Subject: [PATCH 016/148] Fixed empty location due to setting consents later --- Countly.m | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Countly.m b/Countly.m index 0b3620f1..99041e78 100644 --- a/Countly.m +++ b/Countly.m @@ -187,20 +187,7 @@ - (void)startWithConfig:(CountlyConfig *)config [CountlyConnectionManager.sharedInstance beginSession]; //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. - if (!CountlyConsentManager.sharedInstance.consentForSessions) - { - //Send an empty location if location is disabled or location consent is not given, without checking for location consent. - if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - [CountlyConnectionManager.sharedInstance sendLocationInfo]; - } - else - { - [CountlyLocationManager.sharedInstance sendLocationInfo]; - } - [CountlyConnectionManager.sharedInstance sendAttribution]; - } - + #if (TARGET_OS_IOS || TARGET_OS_OSX) #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS if ([config.features containsObject:CLYPushNotifications]) @@ -281,6 +268,21 @@ - (void)startWithConfig:(CountlyConfig *)config else if (config.consents) [self giveConsentForFeatures:config.consents]; + if (!CountlyConsentManager.sharedInstance.consentForSessions) + { + //Send an empty location if location is disabled or location consent is not given, without checking for location consent. + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } + else + { + [CountlyLocationManager.sharedInstance sendLocationInfo]; + } + [CountlyConnectionManager.sharedInstance sendAttribution]; + } + + if (config.campaignType && config.campaignData) [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; From 21ac26fb26fc3d8c3d9bc468105c84119269532e Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 29 Jul 2024 11:22:57 +0500 Subject: [PATCH 017/148] Tests added for location --- Countly.xcodeproj/project.pbxproj | 4 + CountlyTests/CountlyLocationTests.swift | 259 ++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 CountlyTests/CountlyLocationTests.swift diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index f1707279..1b9594ba 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 39924ED02BEBD0D400139F91 /* CountlyCrashesConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ECF2BEBD0D400139F91 /* CountlyCrashesConfig.m */; }; 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */; }; 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ED72BEBD22100139F91 /* CountlyCrashData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399B464F2C52813700AD384E /* CountlyLocationTests.swift */; }; 3B20A9872245225A00E3D7AE /* Countly.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9852245225A00E3D7AE /* Countly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A98D2245228300E3D7AE /* CountlyConnectionManager.h */; }; 3B20A9B32245228700E3D7AE /* CountlyNotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B20A98E2245228300E3D7AE /* CountlyNotificationService.m */; }; @@ -117,6 +118,7 @@ 39924ECF2BEBD0D400139F91 /* CountlyCrashesConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashesConfig.m; sourceTree = ""; }; 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashData.m; sourceTree = ""; }; 39924ED72BEBD22100139F91 /* CountlyCrashData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyCrashData.h; sourceTree = ""; }; + 399B464F2C52813700AD384E /* CountlyLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyLocationTests.swift; sourceTree = ""; }; 3B20A9822245225A00E3D7AE /* Countly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Countly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B20A9852245225A00E3D7AE /* Countly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Countly.h; sourceTree = ""; }; 3B20A9862245225A00E3D7AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -190,6 +192,7 @@ 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */, 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */, 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */, + 399B464F2C52813700AD384E /* CountlyLocationTests.swift */, ); path = CountlyTests; sourceTree = ""; @@ -418,6 +421,7 @@ buildActionMask = 2147483647; files = ( 1A5C4C972B35B0850032EE1F /* CountlyTests.swift in Sources */, + 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */, 1A50D7052B3C5AA3009C6938 /* CountlyBaseTestCase.swift in Sources */, 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */, 968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */, diff --git a/CountlyTests/CountlyLocationTests.swift b/CountlyTests/CountlyLocationTests.swift new file mode 100644 index 00000000..55eb1178 --- /dev/null +++ b/CountlyTests/CountlyLocationTests.swift @@ -0,0 +1,259 @@ +// +// CountlyLocationTests.swift +// CountlyTests +// +// Created by Muhammad Junaid Akram on 25/07/2024. +// Copyright © 2024 Countly. All rights reserved. +// + +import XCTest +@testable import Countly + +// M:Manual Sessions enabled +// A:Automatic sessions enabled +// H:Hybrid Sessions enabled +// CR:Consent Required +// CNR:Consent not Required +// CG:Consent given (All) +// CNG:Consent not given (All) +// CGS:Consent given for session +// CGL:Consent givent for location +// LD:Location Disable +// L: Location Provided + +class CountlyLocationTests: CountlyBaseTestCase { + + func testDummy() { + } + + func testLocationInit_CNR_A() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[0].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CR_CNG_A() throws { + let config = createBaseConfig() + config.requiresConsent = true + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertFalse(queuedRequests[0].contains("begin_session=1"), "Begin session should not start session consent is not given.") + XCTAssertTrue(queuedRequests[1].contains("location="), "Individual location request should send in this scenario") + } + + func testLocationInit_CR_CG_A() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.enableAllConsents = true + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[0].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CR_CGS_A() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.consents = [CLYConsent.sessions]; + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + XCTAssertTrue(queuedRequests[0].contains("location="), "Location should send in this scenario") + } + + func testLocationInit_CR_CGL_A() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.consents = [CLYConsent.location]; + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[0].contains("consent="), "Only consent request should send in this scenario") + } + + func testLocationInit_CR_CGLS_A() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.consents = [CLYConsent.location, CLYConsent.sessions]; + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[0].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CNR_A_L() throws { + let config = createBaseConfig() + config.location = CLLocationCoordinate2D(latitude:35.6895, longitude: 139.6917) + config.city = "Tokyo" + config.isoCountryCode = "JP" + config.ip = "255.255.255.255" + Countly.sharedInstance().start(with: config); + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + + let parsedRequest = parseQueryString(queuedRequests[0]) + + XCTAssertTrue((parsedRequest["location"] as! String) == "35.689500,139.691700", "Begin session should contains provided location") + XCTAssertTrue((parsedRequest["city"] as! String) == "Tokyo", "Begin session should contains provided city") + XCTAssertTrue((parsedRequest["country_code"] as! String) == "JP", "Begin session should contains provided country code") + XCTAssertTrue((parsedRequest["ip_address"] as! String) == "255.255.255.255", "Begin session should contains provided IP address") + } + + func testLocationInit_CNR_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config); + + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[0].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CR_CNG_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertFalse(queuedRequests[0].contains("begin_session=1"), "Begin session should not start session consent is not given.") + XCTAssertTrue(queuedRequests[1].contains("location="), "Individual location request should send in this scenario") + } + + func testLocationInit_CR_CG_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.enableAllConsents = true + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[1].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[1].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CR_CGS_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.consents = [CLYConsent.sessions]; + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[1].contains("begin_session=1"), "Begin session failed.") + XCTAssertTrue(queuedRequests[1].contains("location="), "Location should send in this scenario") + } + + func testLocationInit_CR_CGL_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.consents = [CLYConsent.location]; + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[0].contains("consent="), "Only consent request should send in this scenario") + } + + func testLocationInit_CR_CGLS_M() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.consents = [CLYConsent.location, CLYConsent.sessions]; + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + XCTAssertTrue(queuedRequests[1].contains("begin_session=1"), "Begin session failed.") + XCTAssertFalse(queuedRequests[1].contains("location="), "Location should not be send in this scenario") + } + + func testLocationInit_CNR_M_L() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.location = CLLocationCoordinate2D(latitude:35.6895, longitude: 139.6917) + config.city = "Tokyo" + config.isoCountryCode = "JP" + config.ip = "255.255.255.255" + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().beginSession() + + // get request queue + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + XCTAssertTrue(queuedRequests[0].contains("begin_session=1"), "Begin session failed.") + + let parsedRequest = parseQueryString(queuedRequests[0]) + + XCTAssertTrue((parsedRequest["location"] as! String) == "35.689500,139.691700", "Begin session should contains provided location") + XCTAssertTrue((parsedRequest["city"] as! String) == "Tokyo", "Begin session should contains provided city") + XCTAssertTrue((parsedRequest["country_code"] as! String) == "JP", "Begin session should contains provided country code") + XCTAssertTrue((parsedRequest["ip_address"] as! String) == "255.255.255.255", "Begin session should contains provided IP address") + } + +} + From 23e2f3e30bec48578cb9d6d78c471f88954e2935 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 29 Jul 2024 12:01:23 +0500 Subject: [PATCH 018/148] Added platforms checks for background things --- CountlyConnectionManager.m | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 5d80367c..46d7b762 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -355,10 +355,23 @@ - (void)beginSession return; } +#if TARGET_OS_IOS || TARGET_OS_TV if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); return; } +#elif TARGET_OS_OSX + if (!CountlyCommon.sharedInstance.manualSessionHandling && ![NSApplication sharedApplication].isActive) { + CLY_LOG_W(@"%s App is not active, 'beginSession' will be ignored", __FUNCTION__); + return; + } +#elif TARGET_OS_WATCH + if (!CountlyCommon.sharedInstance.manualSessionHandling && [WKExtension sharedExtension].applicationState == WKApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); + return; + } +#endif + isSessionStarted = YES; lastSessionStartTime = NSDate.date.timeIntervalSince1970; From 6c4e41418218de56673022f970a06d6c3e981fa5 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 29 Jul 2024 20:00:13 +0500 Subject: [PATCH 019/148] Fix orientation not sending if previously consent was not given --- CountlyCommon.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CountlyCommon.m b/CountlyCommon.m index 13d98141..ccbd8cea 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -221,10 +221,11 @@ - (void)recordOrientation } CLY_LOG_D(@"Interface orientation is now: %@", mode); - self.lastInterfaceOrientation = mode; if (!CountlyConsentManager.sharedInstance.consentForUserDetails) return; + + self.lastInterfaceOrientation = mode; [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventOrientation segmentation:@{kCountlyOrientationKeyMode: mode}]; #endif From 7f17ab82d33816c223a29fbf361105bb62b5fdaa Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 30 Jul 2024 14:06:17 +0500 Subject: [PATCH 020/148] Added background change when recording orientation Also sending it with every begin session --- Countly.m | 4 ++-- CountlyCommon.m | 8 +++++++- CountlyConnectionManager.m | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Countly.m b/Countly.m index ba0a5398..5151b42a 100644 --- a/Countly.m +++ b/Countly.m @@ -199,8 +199,8 @@ - (void)startWithConfig:(CountlyConfig *)config if (!CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance beginSession]; - - [CountlyCommon.sharedInstance recordOrientation]; + else + [CountlyCommon.sharedInstance recordOrientation]; //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. diff --git a/CountlyCommon.m b/CountlyCommon.m index ccbd8cea..c673562c 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -196,7 +196,13 @@ - (void)deviceOrientationDidChange:(NSNotification *)notification - (void)recordOrientation { #if (TARGET_OS_IOS) - + if (!self.enableOrientationTracking) + return; + + if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'Record Orientation' will be ignored", __FUNCTION__); + return; + } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 46d7b762..c931ff6e 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -390,7 +390,9 @@ - (void)beginSession queryString = [queryString stringByAppendingString:attributionQueryString]; [CountlyPersistency.sharedInstance addToQueue:queryString]; - + + [CountlyCommon.sharedInstance recordOrientation]; + [self proceedOnQueue]; } From 04cedc0ce5f3132fdb4978ce9061f0231203d8e7 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 30 Jul 2024 19:03:28 +0500 Subject: [PATCH 021/148] Ending session when session consent is revoked --- CountlyConsentManager.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 4dee203d..6299f623 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -143,7 +143,10 @@ - (void)cancelConsentForFeatures:(NSArray *)features shouldSkipSendingConsentsRe return; if ([features containsObject:CLYConsentSessions] && self.consentForSessions) + { + [CountlyConnectionManager.sharedInstance endSession]; self.consentForSessions = NO; + } if ([features containsObject:CLYConsentEvents] && self.consentForEvents) self.consentForEvents = NO; From 302593d44ea1ebcffcfe013065f106bbbedad5ab Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 30 Jul 2024 20:25:13 +0500 Subject: [PATCH 022/148] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba7d2bdc..f72bdf6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ * Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. * Orientation is now also sent during initialization and when consent is given. * Mitigated an issue where consent was not sent when requires consent was true, but no consent was given -* Mitigated an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent. +* Mitigated an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent +* Mitigated an issue where a session could have continued when session consent was removed * Mitigated an issue where empty location was not being sent when location is disabled or location consent is not given From 00cf51408b7c7320de6be4cbf89ba34053ad40ef Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:07:51 +0900 Subject: [PATCH 023/148] ch --- CHANGELOG.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72bdf6e..377c5617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,10 @@ ## 24.7.1 -* Added `enableTemporaryDeviceIDMode` methods both at the Config level and after initialization to enable temporary device ID mode. -* Orientation is now also sent during initialization and when consent is given. -* Mitigated an issue where consent was not sent when requires consent was true, but no consent was given -* Mitigated an automatic start session issue where a session could be started while the app was in the background, resulting in 0 duration sessions being sent -* Mitigated an issue where a session could have continued when session consent was removed -* Mitigated an issue where empty location was not being sent when location is disabled or location consent is not given - +* Added `enableTemporaryDeviceIDMode` config and post-initialization methods to enable temporary device ID mode +* Orientation info is now also sent during initialization +* Mitigated an issue where consent information was not sent when no consent was given during initialization +* Mitigated an issue where a session could have started if the SDK was initialized on the background and automatic session tracking was enabled +* Mitigated an issue where a session did not end when session consent was removed +* Mitigated an issue where disabling location did not work ## 24.7.0 * Implemented automatic sending of user properties to the server without requiring an explicit call to the `save` method From ae9ffe001280e8b1ba9e2ae784ea089725e4ceab Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 31 Jul 2024 15:08:40 +0500 Subject: [PATCH 024/148] Fixed user profile tests --- CountlyTests/CountlyUserProfileTests.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CountlyTests/CountlyUserProfileTests.swift b/CountlyTests/CountlyUserProfileTests.swift index 3ff8bd22..3740b41c 100644 --- a/CountlyTests/CountlyUserProfileTests.swift +++ b/CountlyTests/CountlyUserProfileTests.swift @@ -67,7 +67,7 @@ class CountlyUserProfileTests: CountlyBaseTestCase { Countly.sharedInstance().start(with: config); sendUserProperty() setUserData() - XCTAssertEqual(0, CountlyPersistency.sharedInstance().remainingRequestCount()) + XCTAssertEqual(2, CountlyPersistency.sharedInstance().remainingRequestCount()) // consents, location } func test_203_CNR_A() { @@ -151,8 +151,7 @@ class CountlyUserProfileTests: CountlyBaseTestCase { Countly.sharedInstance().recordEvent("D"); setSameData() Countly.sharedInstance().recordEvent("E"); - - XCTAssertEqual(0, CountlyPersistency.sharedInstance().remainingRequestCount()) + XCTAssertEqual(2, CountlyPersistency.sharedInstance().remainingRequestCount()) // consents, location } func test_207_CNR_M() { @@ -229,7 +228,7 @@ class CountlyUserProfileTests: CountlyBaseTestCase { Countly.sharedInstance().recordEvent("D"); - XCTAssertEqual(9, CountlyPersistency.sharedInstance().remainingRequestCount()) + XCTAssertEqual(10, CountlyPersistency.sharedInstance().remainingRequestCount()) guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { fatalError("Failed to get queuedRequests from CountlyPersistency") } @@ -243,6 +242,7 @@ class CountlyUserProfileTests: CountlyBaseTestCase { validateCustomUserDetails(request: queuedRequests[6], propertiesToCheck: getUserDataMap()) XCTAssertTrue(queuedRequests[7].contains("device_id=merge_id"), "Merge device id failed") validateCustomUserDetails(request: queuedRequests[8], propertiesToCheck: ["a12345": 4]) + XCTAssertTrue(queuedRequests[9].contains("location="), "Empty location should send in this case.") // XCTAssertTrue(queuedRequests[9].contains("device_id=non_merge_id"), "Non Merge device id failed") // validateCustomUserDetails(request: queuedRequests[9], propertiesToCheck: ["a12345": 4]) @@ -272,12 +272,12 @@ class CountlyUserProfileTests: CountlyBaseTestCase { Countly.sharedInstance().recordEvent("D"); - XCTAssertEqual(1, CountlyPersistency.sharedInstance().remainingRequestCount()) + XCTAssertEqual(3, CountlyPersistency.sharedInstance().remainingRequestCount()) // consents, location, device id change guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { fatalError("Failed to get queuedRequests from CountlyPersistency") } - XCTAssertTrue(queuedRequests[0].contains("device_id=merge_id"), "Merge device id failed") - XCTAssertTrue(queuedRequests[0].contains("old_device_id="), "Merge device id failed") + XCTAssertTrue(queuedRequests[2].contains("device_id=merge_id"), "Merge device id failed") + XCTAssertTrue(queuedRequests[2].contains("old_device_id="), "Merge device id failed") } // Test case for Consent Not Required with Manual Sessions enabled From 3568bb060df10205e5bb160795bcdd1958707af0 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 31 Jul 2024 16:15:24 +0500 Subject: [PATCH 025/148] these observers are not needed for macos --- Countly.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Countly.m b/Countly.m index 5151b42a..de11d7bd 100644 --- a/Countly.m +++ b/Countly.m @@ -72,13 +72,6 @@ - (instancetype)init selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification - object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationWillResignActive:) name:NSApplicationWillResignActiveNotification - object:nil]; #endif } From 244dc56ca247241d73cf5e70433d208dcc26dae4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 12 Aug 2024 12:52:47 +0500 Subject: [PATCH 026/148] Implemented customize webview --- Countly.xcodeproj/project.pbxproj | 16 +++ CountlyWebViewManager.h | 44 ++++++++ CountlyWebViewManager.m | 172 ++++++++++++++++++++++++++++++ PassThroughBackgroundView.h | 17 +++ PassThroughBackgroundView.m | 32 ++++++ 5 files changed, 281 insertions(+) create mode 100644 CountlyWebViewManager.h create mode 100644 CountlyWebViewManager.m create mode 100644 PassThroughBackgroundView.h create mode 100644 PassThroughBackgroundView.m diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 1b9594ba..ec9cf95e 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -27,6 +27,10 @@ 3948A8582BAC2E7D002D09AA /* CountlySDKLimitsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3948A8562BAC2E7D002D09AA /* CountlySDKLimitsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39527E152B5FD27400EE5D7B /* CountlyAPMConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39527E142B5FD27400EE5D7B /* CountlyAPMConfig.m */; }; 39527E182B5FD54C00EE5D7B /* CountlyAPMConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39527E162B5FD28900EE5D7B /* CountlyAPMConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3961C6B52C6633C000DD38BA /* CountlyWebViewManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */; }; + 3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3961C6B12C6633C000DD38BA /* PassThroughBackgroundView.h */; }; + 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3961C6B32C6633C000DD38BA /* PassThroughBackgroundView.m */; }; + 3961C6BA2C6633C000DD38BA /* CountlyWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */; }; 3964A3E72C2AF8E90091E677 /* CountlySegmentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */; }; 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */; }; 3972EDDB2C08A38D00EB9D3E /* CountlyEventStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */; }; @@ -109,6 +113,10 @@ 39527E142B5FD27400EE5D7B /* CountlyAPMConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyAPMConfig.m; sourceTree = ""; }; 39527E162B5FD28900EE5D7B /* CountlyAPMConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyAPMConfig.h; sourceTree = ""; }; 395683372BB2988300C7A06B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyWebViewManager.h; sourceTree = ""; }; + 3961C6B12C6633C000DD38BA /* PassThroughBackgroundView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PassThroughBackgroundView.h; sourceTree = ""; }; + 3961C6B32C6633C000DD38BA /* PassThroughBackgroundView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PassThroughBackgroundView.m; sourceTree = ""; }; + 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyWebViewManager.m; sourceTree = ""; }; 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlySegmentationTests.swift; sourceTree = ""; }; 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyDeviceIDTests.swift; sourceTree = ""; }; 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyEventStruct.swift; sourceTree = ""; }; @@ -200,6 +208,10 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( + 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */, + 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */, + 3961C6B12C6633C000DD38BA /* PassThroughBackgroundView.h */, + 3961C6B32C6633C000DD38BA /* PassThroughBackgroundView.m */, 39924ED72BEBD22100139F91 /* CountlyCrashData.h */, 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */, 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */, @@ -292,6 +304,7 @@ 1A478D032AB314750056A5E7 /* CountlyExperimentInformation.h in Headers */, 3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */, 3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */, + 3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */, 3B20A9CA2245228700E3D7AE /* CountlyConfig.h in Headers */, 3B20A9872245225A00E3D7AE /* Countly.h in Headers */, 1A423EA02A271FE0008C4757 /* CountlyRCData.h in Headers */, @@ -299,6 +312,7 @@ 1A3110652A7128ED001CB507 /* CountlyViewData.h in Headers */, 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */, 1A3A576529ED47BD0041B7BE /* CountlyServerConfig.h in Headers */, + 3961C6B52C6633C000DD38BA /* CountlyWebViewManager.h in Headers */, 1ACA5DC12A309E7F001F770B /* CountlyRemoteConfigInternal.h in Headers */, 3B20A9CC2245228700E3D7AE /* CountlyViewTrackingInternal.h in Headers */, 3948A8582BAC2E7D002D09AA /* CountlySDKLimitsConfig.h in Headers */, @@ -449,6 +463,7 @@ 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */, D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */, 1A3110632A7128CD001CB507 /* CountlyViewData.m in Sources */, + 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */, 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, @@ -460,6 +475,7 @@ 3B20A9B42245228700E3D7AE /* CountlyPushNotifications.m in Sources */, 3B20A9C92245228700E3D7AE /* CountlyUserDetails.m in Sources */, 3B20A9D12245228700E3D7AE /* CountlyConfig.m in Sources */, + 3961C6BA2C6633C000DD38BA /* CountlyWebViewManager.m in Sources */, 1A9027FE2AB197B50044EBCF /* CountlyExperimentInformation.m in Sources */, 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */, 3B20A9C82245228700E3D7AE /* CountlyViewTrackingInternal.m in Sources */, diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h new file mode 100644 index 00000000..51edbea1 --- /dev/null +++ b/CountlyWebViewManager.h @@ -0,0 +1,44 @@ +// CountlyWebViewManager.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import +#import + +typedef NS_ENUM(NSInteger, WebViewPosition) { + WebViewPositionTop, + WebViewPositionBottom, + WebViewPositionCenter, + WebViewPositionTopLeft, + WebViewPositionTopRight, + WebViewPositionBottomLeft, + WebViewPositionBottomRight, +}; + +typedef NS_ENUM(NSInteger, WebViewSize) { + WebViewFullScreen, + WebViewHalf, + WebViewBanner, + WebViewSquareSmall +}; + +typedef NS_ENUM(NSUInteger, AnimationType) { + AnimationTypeSlideInFromBottom, + AnimationTypeSlideInFromTop, + AnimationTypeSlideInFromLeft, + AnimationTypeSlideInFromRight, + AnimationTypeIncreaseHeight, + AnimationTypeIncreaseHeightFromBottom +}; + +@interface CountlyWebViewManager : NSObject + + +- (void)createWebViewWithURL:(NSURL *)url + frame:(CGRect)frame + appearBlock:(void(^ __nullable)(void))appearBlock + dismissBlock:(void(^ __nullable)(void))dismissBlock; + +@end diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m new file mode 100644 index 00000000..d91e94e3 --- /dev/null +++ b/CountlyWebViewManager.m @@ -0,0 +1,172 @@ +#import "CountlyWebViewManager.h" +#import "PassThroughBackgroundView.h" +#import "CountlyCommon.h" + +@interface CountlyWebViewManager() +@property (nonatomic, strong) PassThroughBackgroundView *backgroundView; +@property (nonatomic, copy) void (^dismissBlock)(void); +@end + +@implementation CountlyWebViewManager + +- (void)createWebViewWithURL:(NSURL *)url + frame:(CGRect)frame + appearBlock:(void(^ __nullable)(void))appearBlock + dismissBlock:(void(^ __nullable)(void))dismissBlock { + self.dismissBlock = dismissBlock; + UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController; + CGRect backgroundFrame = rootViewController.view.bounds; + + if (@available(iOS 11.0, *)) { + CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; + backgroundFrame.origin.y += top ? top + 5 : 20.0; + backgroundFrame.size.height -= top ? top + 5 : 20.0; + } else { + backgroundFrame.origin.y += 20.0; + backgroundFrame.size.height -= 20.0; + } + + self.backgroundView = [[PassThroughBackgroundView alloc] initWithFrame:backgroundFrame]; + self.backgroundView.backgroundColor = [UIColor clearColor]; + [rootViewController.view addSubview:self.backgroundView]; + + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; + configuration.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration]; + + [self configureWebView:webView]; + + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + [webView loadRequest:request]; + + CLYButton *dismissButton = [CLYButton dismissAlertButton:@"X"]; + [self configureDismissButton:dismissButton forWebView:webView]; + + self.backgroundView.webView = webView; + self.backgroundView.dismissButton = dismissButton; + [webView evaluateJavaScript:@"document.readyState" completionHandler:^(id _Nullable result, NSError * _Nullable error) { + if ([result isKindOfClass:[NSString class]] && [(NSString *)result isEqualToString:@"complete"]) { + NSLog(@"Web view has finished loading"); + self.backgroundView.hidden = NO; + if (appearBlock) { + appearBlock(); + } + } + }]; +} + +- (void)configureWebView:(WKWebView *)webView { + webView.layer.shadowColor = UIColor.blackColor.CGColor; + webView.layer.shadowOpacity = 0.5; + webView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); + webView.layer.masksToBounds = NO; + webView.opaque = NO; + webView.scrollView.bounces = NO; + webView.navigationDelegate = self; + + [self.backgroundView addSubview:webView]; +} + +- (void)configureDismissButton:(CLYButton *)dismissButton forWebView:(WKWebView *)webView { + dismissButton.onClick = ^(id sender) { + if (self.dismissBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.dismissBlock(); + [self.backgroundView removeFromSuperview]; + }); + } + }; + + [self.backgroundView addSubview:dismissButton]; + [dismissButton positionToTopRight]; + [self.backgroundView bringSubviewToFront:webView]; + [webView bringSubviewToFront:dismissButton]; + + self.backgroundView.dismissButton = dismissButton; + dismissButton.hidden = YES; +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSString *url = navigationAction.request.URL.absoluteString; + + if ([url containsString:@"cly_x_int=1"]) { + CLY_LOG_I(@"%s Opening URL [%@] in external browser", __FUNCTION__, url); + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { + CLY_LOG_I(success ? @"%s URL [%@] opened in external browser" : @"%s Unable to open URL [%@] in external browser", __FUNCTION__, url); + }]; + decisionHandler(WKNavigationActionPolicyCancel); + } else if ([url containsString:@"cly_x_close=1"]) { + CLY_LOG_I(@"%s Closing webview", __FUNCTION__); + if (self.dismissBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.dismissBlock(); + [self.backgroundView removeFromSuperview]; + }); + } + decisionHandler(WKNavigationActionPolicyCancel); + } else { + decisionHandler(WKNavigationActionPolicyAllow); + } +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + CLY_LOG_I(@"%s Web view has started loading", __FUNCTION__); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); +} + +- (void)animateView:(UIView *)view withAnimationType:(AnimationType)animationType { + NSTimeInterval animationDuration = 0; + CGAffineTransform initialTransform = CGAffineTransformIdentity; + + switch (animationType) { + case AnimationTypeSlideInFromBottom: + initialTransform = CGAffineTransformMakeTranslation(0, view.superview.frame.size.height); + break; + case AnimationTypeSlideInFromTop: + initialTransform = CGAffineTransformMakeTranslation(0, -view.superview.frame.size.height); + break; + case AnimationTypeSlideInFromLeft: + initialTransform = CGAffineTransformMakeTranslation(-view.superview.frame.size.width, 0); + break; + case AnimationTypeSlideInFromRight: + initialTransform = CGAffineTransformMakeTranslation(view.superview.frame.size.width, 0); + break; + case AnimationTypeIncreaseHeight: { + CGRect originalFrame = view.frame; + view.frame = CGRectMake(originalFrame.origin.x, originalFrame.origin.y, originalFrame.size.width, 0); + [UIView animateWithDuration:animationDuration animations:^{ + view.frame = originalFrame; + }]; + return; + } + default: + return; + } + + view.transform = initialTransform; + [UIView animateWithDuration:animationDuration animations:^{ + view.transform = CGAffineTransformIdentity; + }]; +} + +CGSize sizeForWebViewSize(WebViewSize size, CGRect backgroundFrame, UIEdgeInsets padding) { + CGFloat width = backgroundFrame.size.width - padding.left - padding.right; + CGFloat height = backgroundFrame.size.height - padding.top - padding.bottom; + + switch (size) { + case WebViewFullScreen: + return CGSizeMake(width, height); + case WebViewHalf: + return CGSizeMake(width, height / 2); + case WebViewBanner: + return CGSizeMake(width, 70); + case WebViewSquareSmall: + return CGSizeMake(250, 250); + default: + return CGSizeMake(width, height); + } +} +@end diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h new file mode 100644 index 00000000..76614706 --- /dev/null +++ b/PassThroughBackgroundView.h @@ -0,0 +1,17 @@ +// PassThroughBackgroundView.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import +#import +#import "CountlyCommon.h" + +@interface PassThroughBackgroundView : UIView + +@property (nonatomic, strong) WKWebView *webView; +@property (nonatomic, strong) CLYButton *dismissButton; + + +@end diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m new file mode 100644 index 00000000..2ba419b4 --- /dev/null +++ b/PassThroughBackgroundView.m @@ -0,0 +1,32 @@ +// PassThroughBackgroundView.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "PassThroughBackgroundView.h" + +@implementation PassThroughBackgroundView + +@synthesize webView; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + } + return self; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + + if (self.webView && CGRectContainsPoint(self.webView.frame, point)) { + return YES; + } + if (self.dismissButton && CGRectContainsPoint(self.dismissButton.frame, point)) { + return YES; + } + + return NO; +} + +@end From 5bfbc1f0382f65e7e2f980a866e9e24925493c75 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 12 Aug 2024 14:15:04 +0500 Subject: [PATCH 027/148] Implemented Content Builder Changes --- Countly.h | 7 + Countly.m | 15 ++ Countly.xcodeproj/project.pbxproj | 16 ++ CountlyConfig.h | 5 + CountlyContentBuilder.h | 19 +++ CountlyContentBuilder.m | 49 ++++++ CountlyContentBuilderInternal.h | 25 +++ CountlyContentBuilderInternal.m | 247 ++++++++++++++++++++++++++++++ 8 files changed, 383 insertions(+) create mode 100644 CountlyContentBuilder.h create mode 100644 CountlyContentBuilder.m create mode 100644 CountlyContentBuilderInternal.h create mode 100644 CountlyContentBuilderInternal.m diff --git a/Countly.h b/Countly.h index de5c8241..d702d703 100644 --- a/Countly.h +++ b/Countly.h @@ -12,6 +12,7 @@ #import "CountlyRemoteConfig.h" #import "CountlyFeedbackWidget.h" #import "CountlyViewTracking.h" +#import "CountlyContentBuilder.h" #import "Resettable.h" #if (TARGET_OS_IOS || TARGET_OS_OSX) #import @@ -829,4 +830,10 @@ NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END +/** + * Interface variable to access views tracking functionalities. + * @discussion Views tracking interface for developer to interact with SDK. + */ +- (CountlyContentBuilder *_Nonnull) content; + @end diff --git a/Countly.m b/Countly.m index de11d7bd..57b8d493 100644 --- a/Countly.m +++ b/Countly.m @@ -264,6 +264,16 @@ - (void)startWithConfig:(CountlyConfig *)config appLoadStartTime = config.apm.getAppStartTimestampOverride; } + // Delay in seconds + double delayInSeconds = 1.0; // Adjust the delay time as needed + + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(delay, dispatch_get_main_queue(), ^{ + if(config.enableContentUpdates) { + [[self content] openForContent]; + } + }); + [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; @@ -1432,4 +1442,9 @@ - (void)attemptToSendStoredRequests [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; } +- (CountlyContentBuilder *) content +{ + return CountlyContentBuilder.sharedInstance; +} + @end diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index ec9cf95e..d40d7984 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -35,6 +35,10 @@ 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */; }; 3972EDDB2C08A38D00EB9D3E /* CountlyEventStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */; }; 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */; }; + 399117D12C69F73D00DC4C66 /* CountlyContentBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */; }; + 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 399117CE2C69F73D00DC4C66 /* CountlyContentBuilderInternal.h */; }; + 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CF2C69F73D00DC4C66 /* CountlyContentBuilderInternal.m */; }; + 399117D42C69F73D00DC4C66 /* CountlyContentBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 399117D02C69F73D00DC4C66 /* CountlyContentBuilder.h */; }; 39911B672B457DBB00AC053C /* Resettable.h in Headers */ = {isa = PBXBuildFile; fileRef = 39911B662B457DB500AC053C /* Resettable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39924ECE2BEBD0B700139F91 /* CountlyCrashesConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39924ED02BEBD0D400139F91 /* CountlyCrashesConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ECF2BEBD0D400139F91 /* CountlyCrashesConfig.m */; }; @@ -121,6 +125,10 @@ 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyDeviceIDTests.swift; sourceTree = ""; }; 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyEventStruct.swift; sourceTree = ""; }; 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyUserProfileTests.swift; sourceTree = ""; }; + 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentBuilder.m; sourceTree = ""; }; + 399117CE2C69F73D00DC4C66 /* CountlyContentBuilderInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyContentBuilderInternal.h; sourceTree = ""; }; + 399117CF2C69F73D00DC4C66 /* CountlyContentBuilderInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentBuilderInternal.m; sourceTree = ""; }; + 399117D02C69F73D00DC4C66 /* CountlyContentBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyContentBuilder.h; sourceTree = ""; }; 39911B662B457DB500AC053C /* Resettable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Resettable.h; sourceTree = ""; }; 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyCrashesConfig.h; sourceTree = ""; }; 39924ECF2BEBD0D400139F91 /* CountlyCrashesConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashesConfig.m; sourceTree = ""; }; @@ -212,6 +220,10 @@ 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */, 3961C6B12C6633C000DD38BA /* PassThroughBackgroundView.h */, 3961C6B32C6633C000DD38BA /* PassThroughBackgroundView.m */, + 399117D02C69F73D00DC4C66 /* CountlyContentBuilder.h */, + 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */, + 399117CE2C69F73D00DC4C66 /* CountlyContentBuilderInternal.h */, + 399117CF2C69F73D00DC4C66 /* CountlyContentBuilderInternal.m */, 39924ED72BEBD22100139F91 /* CountlyCrashData.h */, 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */, 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */, @@ -320,6 +332,7 @@ D219374B248AC71C00E5798B /* CountlyPerformanceMonitoring.h in Headers */, 39911B672B457DBB00AC053C /* Resettable.h in Headers */, 3B20A9BC2245228700E3D7AE /* CountlyCommon.h in Headers */, + 399117D42C69F73D00DC4C66 /* CountlyContentBuilder.h in Headers */, 39527E182B5FD54C00EE5D7B /* CountlyAPMConfig.h in Headers */, 3B20A9C52245228700E3D7AE /* CountlyCrashReporter.h in Headers */, 3B20A9BE2245228700E3D7AE /* CountlyDeviceInfo.h in Headers */, @@ -330,6 +343,7 @@ 3B20A9B72245228700E3D7AE /* CountlyPersistency.h in Headers */, D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */, 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */, + 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */, D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -464,6 +478,7 @@ D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */, 1A3110632A7128CD001CB507 /* CountlyViewData.m in Sources */, 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */, + 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */, 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, @@ -474,6 +489,7 @@ D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */, 3B20A9B42245228700E3D7AE /* CountlyPushNotifications.m in Sources */, 3B20A9C92245228700E3D7AE /* CountlyUserDetails.m in Sources */, + 399117D12C69F73D00DC4C66 /* CountlyContentBuilder.m in Sources */, 3B20A9D12245228700E3D7AE /* CountlyConfig.m in Sources */, 3961C6BA2C6633C000DD38BA /* CountlyWebViewManager.m in Sources */, 1A9027FE2AB197B50044EBCF /* CountlyExperimentInformation.m in Sources */, diff --git a/CountlyConfig.h b/CountlyConfig.h index fbc2e979..fa60993d 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -656,6 +656,11 @@ typedef enum : NSUInteger * @discussion If set, Server Config values from Countly Server will be fetched at the beginning of a session. */ @property (nonatomic) BOOL enableServerConfiguration; + +/** + * This will enable to fetch content from server and display them in UI + */ +@property (nonatomic) BOOL enableContentUpdates; NS_ASSUME_NONNULL_END @end diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h new file mode 100644 index 00000000..710c7334 --- /dev/null +++ b/CountlyContentBuilder.h @@ -0,0 +1,19 @@ +// CountlyContentBuilder.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import +#import + +@interface CountlyContentBuilder: NSObject + ++ (instancetype)sharedInstance; + +- (void)openForContent; +- (void)openForContent:(NSArray *)tags; +- (void)exitFromContent; +- (void)changeContent:(NSArray *)tags; + +@end diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m new file mode 100644 index 00000000..76692984 --- /dev/null +++ b/CountlyContentBuilder.m @@ -0,0 +1,49 @@ +// CountlyContentBuilder.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyContentBuilder.h" +#import "CountlyContentBuilderInternal.h" +#import "CountlyCommon.h" + +@implementation CountlyContentBuilder + ++ (instancetype)sharedInstance +{ + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + + static CountlyContentBuilder* s_sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; +} + +- (instancetype)init +{ + self = [super init]; + + return self; +} + +- (void)openForContent +{ + [self openForContent:@[]]; +} + +- (void)openForContent:(NSArray *)tags +{ + [CountlyContentBuilderInternal.sharedInstance openForContent:tags]; +} +- (void)exitFromContent +{ + [CountlyContentBuilderInternal.sharedInstance exitFromContent]; +} +- (void)changeContent:(NSArray *)tags +{ + [CountlyContentBuilder.sharedInstance changeContent:tags]; +} + +@end diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h new file mode 100644 index 00000000..4b4bda41 --- /dev/null +++ b/CountlyContentBuilderInternal.h @@ -0,0 +1,25 @@ +// CountlyContent.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import +#import + +@interface CountlyContentBuilderInternal: NSObject + +@property (nonatomic, strong) NSArray *currentTags; +@property (nonatomic, strong) NSString *latestChecksum; +@property (nonatomic, assign) BOOL isContentConsentGiven; +@property (nonatomic, assign) CGFloat density; +@property (nonatomic, assign) NSTimeInterval requestInterval; + ++ (instancetype)sharedInstance; + +- (void)openForContent:(NSArray *)tags; +- (void)exitFromContent; +- (void)changeContent:(NSArray *)tags; + +@end + diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m new file mode 100644 index 00000000..bcefe908 --- /dev/null +++ b/CountlyContentBuilderInternal.m @@ -0,0 +1,247 @@ +// CountlyContent.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. +#import "CountlyContentBuilderInternal.h" +#import "CountlyCommon.h" +#import "CountlyWebViewManager.h" + + +NSString* const kCountlyEndpointContent = @"/i/content/queue"; //TODO: @"/content"; +NSString* const kCountlyCBFetchContent = @"fetch_content"; +NSString* const kCountlyCBCheckAvailbleContents = @"check_available_contents"; + +@implementation CountlyContentBuilderInternal { + BOOL _isRequestQueueLocked; + NSTimer *_requestTimer; +} + ++ (instancetype)sharedInstance { + static CountlyContentBuilderInternal *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (instancetype)init +{ + if (self = [super init]) + { + self.isContentConsentGiven = YES; + self.latestChecksum = nil; + self.density = 1; //TODO: [UIScreen mainScreen].scale; + self.requestInterval = 30.0; + _requestTimer = nil; + } + + return self; +} + +- (void)openForContent:(NSArray *)tags { + if (!self.isContentConsentGiven) { + return; + } + + if(_requestTimer != nil) { + CLY_LOG_I(@"Already open for content, please exit from content first to start again"); + return; + } + self.currentTags = tags; + + [self fetchContentDetailsForContentId:@"contentId"]; //TODO: [self sendContentCheckRequest]; + _requestTimer = [NSTimer scheduledTimerWithTimeInterval:self.requestInterval + target:self + selector:@selector(fetchContentDetailsForContentId) // TODO: sendContentCheckRequest + userInfo:nil + repeats:YES]; +} + +- (void)exitFromContent { + [self clearContentState]; +} + +- (void)changeContent:(NSArray *)tags { + if (![tags isEqualToArray:self.currentTags]) { + [self exitFromContent]; + [self openForContent:tags]; + } +} + +#pragma mark - Private Methods + +- (void)clearContentState { + [_requestTimer invalidate]; + _requestTimer = nil; + self.currentTags = nil; + self.latestChecksum = nil; + _isRequestQueueLocked = NO; +} + +- (void)sendContentCheckRequest { + if (!self.isContentConsentGiven || _isRequestQueueLocked) { + return; + } + + _isRequestQueueLocked = YES; + + // Send request + NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:[self checkContentRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + CLY_LOG_I(@"Content check request failed: %@", error); + self->_isRequestQueueLocked = NO; + return; + } + + // Process the response + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *newChecksum = jsonResponse[@"checksum"]; + NSString *contentId = jsonResponse[@"content_id"]; + + if (newChecksum && ![newChecksum isEqualToString:self.latestChecksum]) { + self.latestChecksum = newChecksum; + [self fetchContentDetailsForContentId:contentId]; + } + + self->_isRequestQueueLocked = NO; + }]; + + [task resume]; +} + +//TODO: remove this method +- (void) fetchContentDetailsForContentId { + [self fetchContentDetailsForContentId:@""]; +} +- (void)fetchContentDetailsForContentId:(NSString *)contentId { + //TODO: removed _isRequestQueueLocked from this method when we are using 'sendContentCheckRequest' + if (_isRequestQueueLocked) { + return; + } + + _isRequestQueueLocked = YES; + + NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:[self fetchContentDetailsRequest:contentId] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + CLY_LOG_I(@"Fetch content details failed: %@", error); + self->_isRequestQueueLocked = NO; + return; + } + + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *pathToHtml = jsonResponse[@"pathToHtml"]; + NSDictionary *placementCoordinates = jsonResponse[@"placementCoordinates"]; + + [self showContentWithHtmlPath:pathToHtml placementCoordinates:placementCoordinates]; + self->_isRequestQueueLocked = NO; + }]; + + [dataTask resume]; +} + +- (NSURLRequest *)checkContentRequest +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + NSString *tagsString = [self.currentTags componentsJoinedByString:@","]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=[%@]", + kCountlyQSKeyMethod, kCountlyCBCheckAvailbleContents, + @"tags", tagsString]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointContent, + queryString]; + + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]; + return request; +} + +- (NSURLRequest *)fetchContentDetailsRequest:(NSString *)content_id +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + NSString *resolutionJson = [self resolutionJson]; + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%@", + kCountlyQSKeyMethod, kCountlyCBFetchContent, + @"content_id", content_id, + @"resolution", resolutionJson]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointContent, + queryString]; + + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]; + return request; +} + +- (NSString *)resolutionJson { + CGRect screenBounds = [UIScreen mainScreen].bounds; + if (@available(iOS 11.0, *)) { + CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; + + if (top) { + screenBounds.origin.y += top + 5; + screenBounds.size.height -= top + 5; + } else { + screenBounds.origin.y += 20.0; + screenBounds.size.height -= 20.0; + } + } else { + screenBounds.origin.y += 20.0; + screenBounds.size.height -= 20.0; + } + CGFloat width = screenBounds.size.width / self.density; + CGFloat height = screenBounds.size.height / self.density; + + NSDictionary *resolutionDict = @{ + @"landscape": @{@"width": @(height), @"height": @(width)}, + @"portrait": @{@"width": @(width), @"height": @(height)} + }; + + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resolutionDict options:0 error:nil]; + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +- (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSDictionary *)placementCoordinates { + // Convert pathToHtml to NSURL + NSURL *url = [NSURL URLWithString:pathToHtml]; + + dispatch_async(dispatch_get_main_queue(), ^ { + // Detect screen orientation + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); + + + // Get the appropriate coordinates based on the orientation + NSDictionary *coordinates = isLandscape ? placementCoordinates[@"landscape"] : placementCoordinates[@"portrait"]; + + // Extract placement coordinates and adjust for screen density + CGFloat x = [coordinates[@"x"] floatValue] / self.density; + CGFloat y = [coordinates[@"y"] floatValue] / self.density; + CGFloat width = [coordinates[@"width"] floatValue] / self.density; + CGFloat height = [coordinates[@"height"] floatValue] / self.density; + + CGRect frame = CGRectMake(x, y, width, height); + + // Log the URL and the frame + CLY_LOG_I(@"Showing content from URL: %@", url); + CLY_LOG_I(@"Placement frame: %@", NSStringFromCGRect(frame)); + + CountlyWebViewManager* webViewManager = CountlyWebViewManager.new; + [webViewManager createWebViewWithURL:url frame:frame appearBlock:^ + { + CLY_LOG_I(@"Webview appeared"); + } dismissBlock:^ + { + CLY_LOG_I(@"Webview dismissed"); + }]; + }); +} +@end From 1479dc588301b1603002d9c512b4a633a5f8c138 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 16 Aug 2024 09:57:40 +0500 Subject: [PATCH 028/148] Stat/stop views instead of pause/resume when app goes to background/foreground --- CountlyViewData.h | 17 +++++++------- CountlyViewData.m | 12 +--------- CountlyViewTrackingInternal.m | 44 ++++++++++++++++------------------- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/CountlyViewData.h b/CountlyViewData.h index ec4fd6c3..d56dc687 100644 --- a/CountlyViewData.h +++ b/CountlyViewData.h @@ -30,10 +30,10 @@ @property (nonatomic) BOOL isAutoStoppedView; /** - * Is this view is automaticaly paused. - * @discussion It sets true when app goes to backround, and view will resume on the basis of that flag when app goes to foreground. + * Is this view is automaticaly stopped. + * @discussion It sets true when app goes to backround, and view will start again on the basis of that flag when app goes to foreground. */ -@property (nonatomic) BOOL isAutoPaused; +@property (nonatomic) BOOL willStartAgain; /** @@ -42,6 +42,12 @@ */ @property (nonatomic) NSMutableDictionary* segmentation; +/** + * Segmentation of start view . + * @discussion This segmentation will store to send again when view is start again when app goes to foreground + */ +@property (nonatomic) NSMutableDictionary* startSegmentation; + /** * Initialize view data * @discussion If set then this view will automatically stopped when new view is started. @@ -56,11 +62,6 @@ */ - (NSTimeInterval)duration; -/** - * Pause view - * @discussion View is paused automatically base on the app background event. - */ -- (void)autoPauseView; /** * Pause view diff --git a/CountlyViewData.m b/CountlyViewData.m index 8f93f97a..28ddb6f3 100644 --- a/CountlyViewData.m +++ b/CountlyViewData.m @@ -17,7 +17,7 @@ - (instancetype)initWithID:(NSString *)viewID viewName:(NSString *)viewName self.viewName = viewName; self.viewStartTime = CountlyCommon.sharedInstance.uniqueTimestamp; self.isAutoStoppedView = false; - self.isAutoPaused = false; + self.willStartAgain = false; } return self; @@ -29,15 +29,6 @@ - (NSTimeInterval)duration return duration; } -- (void)autoPauseView -{ - if (self.viewStartTime) // To check that view is not paused already manually - { - self.isAutoPaused = true; - } - [self pauseView]; -} - - (void)pauseView { if (self.viewStartTime) @@ -48,7 +39,6 @@ - (void)pauseView - (void)resumeView { - self.isAutoPaused = false; self.viewStartTime = CountlyCommon.sharedInstance.uniqueTimestamp; } diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 0d607d05..dec01b53 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -518,37 +518,36 @@ - (void)stopCurrentView } -- (void)pauseAllViewsInternal +- (void)stopRunningViewsInternal { [self.viewDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CountlyViewData * _Nonnull viewData, BOOL * _Nonnull stop) { - [self pauseViewInternal:viewData autoPaused:YES]; + viewData.willStartAgain = YES; + [self stopViewWithIDInternal:viewData.viewID customSegmentation:nil autoPaused:YES]; }]; } - (void)pauseViewInternal:(CountlyViewData*) viewData { - [self pauseViewInternal:viewData autoPaused:NO]; -} - -- (void)pauseViewInternal:(CountlyViewData*) viewData autoPaused:(BOOL) autoPaused -{ - if (autoPaused) { - [viewData autoPauseView]; - } - else { - [viewData pauseView]; - } + [viewData pauseView]; [self stopViewWithIDInternal:viewData.viewID customSegmentation:nil autoPaused:YES]; } -- (void)resumeAllViewsInternal +- (void)startStoppedViewsInternal { + // Create an array to store keys for views that need to be removed + NSMutableArray *keysToRemove = [NSMutableArray array]; + [self.viewDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CountlyViewData * _Nonnull viewData, BOOL * _Nonnull stop) { - if (viewData.isAutoPaused) + if (viewData.willStartAgain) { - [viewData resumeView]; + [self startViewInternal:viewData.viewName customSegmentation:viewData.segmentation isAutoStoppedView:viewData.isAutoStoppedView]; + // Add the key to the array for later removal + [keysToRemove addObject:viewData.viewID]; } }]; + + // Remove the entries from the dictionary + [self.viewDataDictionary removeObjectsForKeys:keysToRemove]; } - (void)stopAllViewsInternal:(NSDictionary *)segmentation @@ -717,14 +716,11 @@ - (NSString*)titleForViewController:(UIViewController *)viewController - (void)applicationWillEnterForeground { #if (TARGET_OS_IOS || TARGET_OS_TV) - if (self.isAutoViewTrackingActive) { - - } - else { - [self resumeAllViewsInternal]; + if (!self.isAutoViewTrackingActive) { + [self startStoppedViewsInternal]; } #else - [self resumeAllViewsInternal]; + [self startStoppedViewsInternal]; #endif } - (void)applicationDidEnterBackground { @@ -733,10 +729,10 @@ - (void)applicationDidEnterBackground { [self stopCurrentView]; } else { - [self pauseAllViewsInternal]; + [self stopRunningViewsInternal]; } #else - [self pauseAllViewsInternal]; + [self stopRunningViewsInternal]; #endif } From 386671df5d2f24380b58d6331971af5509b28c48 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 16 Aug 2024 10:04:09 +0500 Subject: [PATCH 029/148] Copy segmentation from old view when starting again in case of app goes to foreground --- CountlyViewTrackingInternal.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index dec01b53..f2366c94 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -441,6 +441,7 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio self.currentViewID = CountlyCommon.sharedInstance.randomEventID; CountlyViewData *viewData = [[CountlyViewData alloc] initWithID:self.currentViewID viewName:viewName]; + viewData.startSegmentation = customSegmentation.mutableCopy; viewData.isAutoStoppedView = isAutoStoppedView; self.viewDataDictionary[self.currentViewID] = viewData; @@ -540,8 +541,15 @@ - (void)startStoppedViewsInternal [self.viewDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CountlyViewData * _Nonnull viewData, BOOL * _Nonnull stop) { if (viewData.willStartAgain) { - [self startViewInternal:viewData.viewName customSegmentation:viewData.segmentation isAutoStoppedView:viewData.isAutoStoppedView]; - // Add the key to the array for later removal + NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:viewData.startSegmentation isAutoStoppedView:viewData.isAutoStoppedView]; + + // Retrieve the newly created viewData for the viewID + CountlyViewData* viewDataNew = self.viewDataDictionary[viewID]; + + // Copy the segmentation data from the old view to the new view + viewDataNew.segmentation = viewData.segmentation.mutableCopy; + + // Add the old view's ID to the array for removal later [keysToRemove addObject:viewData.viewID]; } }]; From 1cfb5f0fc71fa58c7557e8b2522c09bc059c860c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 16 Aug 2024 10:11:52 +0500 Subject: [PATCH 030/148] Removed start view segmentation caching --- CountlyViewData.h | 6 ------ CountlyViewTrackingInternal.m | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CountlyViewData.h b/CountlyViewData.h index d56dc687..04c883dc 100644 --- a/CountlyViewData.h +++ b/CountlyViewData.h @@ -42,12 +42,6 @@ */ @property (nonatomic) NSMutableDictionary* segmentation; -/** - * Segmentation of start view . - * @discussion This segmentation will store to send again when view is start again when app goes to foreground - */ -@property (nonatomic) NSMutableDictionary* startSegmentation; - /** * Initialize view data * @discussion If set then this view will automatically stopped when new view is started. diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index f2366c94..f2150792 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -441,7 +441,6 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio self.currentViewID = CountlyCommon.sharedInstance.randomEventID; CountlyViewData *viewData = [[CountlyViewData alloc] initWithID:self.currentViewID viewName:viewName]; - viewData.startSegmentation = customSegmentation.mutableCopy; viewData.isAutoStoppedView = isAutoStoppedView; self.viewDataDictionary[self.currentViewID] = viewData; @@ -541,7 +540,7 @@ - (void)startStoppedViewsInternal [self.viewDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CountlyViewData * _Nonnull viewData, BOOL * _Nonnull stop) { if (viewData.willStartAgain) { - NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:viewData.startSegmentation isAutoStoppedView:viewData.isAutoStoppedView]; + NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:nil isAutoStoppedView:viewData.isAutoStoppedView]; // Retrieve the newly created viewData for the viewID CountlyViewData* viewDataNew = self.viewDataDictionary[viewID]; From 43c018f228b834baa882257f25ceaa33732a3898 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 16 Aug 2024 10:46:41 +0500 Subject: [PATCH 031/148] No need to send old view segmentation when starting a view again --- CountlyViewTrackingInternal.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index f2150792..30bf7eb9 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -542,12 +542,6 @@ - (void)startStoppedViewsInternal { NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:nil isAutoStoppedView:viewData.isAutoStoppedView]; - // Retrieve the newly created viewData for the viewID - CountlyViewData* viewDataNew = self.viewDataDictionary[viewID]; - - // Copy the segmentation data from the old view to the new view - viewDataNew.segmentation = viewData.segmentation.mutableCopy; - // Add the old view's ID to the array for removal later [keysToRemove addObject:viewData.viewID]; } From 939b5522f0bf429cfbf620275be0bced82e66569 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 16 Aug 2024 10:52:13 +0500 Subject: [PATCH 032/148] Added changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 377c5617..50f87a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## xx.xx.xx +* Views will now be stopped and started again when the app transitions between background and foreground, instead of being paused and resumed. + ## 24.7.1 * Added `enableTemporaryDeviceIDMode` config and post-initialization methods to enable temporary device ID mode * Orientation info is now also sent during initialization From d2cd98293695baa4dc0886bf1546d136980f9369 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 19 Aug 2024 18:14:34 +0500 Subject: [PATCH 033/148] Implemented global content callback --- Countly.m | 4 ++++ CountlyCommon.h | 1 + CountlyConfig.h | 20 ++++++++++++++++++++ CountlyConfig.m | 11 +++++++++++ CountlyContentBuilderInternal.h | 2 ++ CountlyContentBuilderInternal.m | 4 +++- 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Countly.m b/Countly.m index 57b8d493..e6f55fb0 100644 --- a/Countly.m +++ b/Countly.m @@ -264,6 +264,10 @@ - (void)startWithConfig:(CountlyConfig *)config appLoadStartTime = config.apm.getAppStartTimestampOverride; } + if(config.getGlobalContentCallback) { + CountlyContentBuilderInternal.sharedInstance.contentCallback = config.getGlobalContentCallback; + } + // Delay in seconds double delayInSeconds = 1.0; // Adjust the delay time as needed diff --git a/CountlyCommon.h b/CountlyCommon.h index 9eff5880..6e6c81cc 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -29,6 +29,7 @@ #import "CountlyViewTracking.h" #import "Resettable.h" #import "CountlyCrashData.h" +#import "CountlyContentBuilderInternal.h" #define CLY_LOG_E(fmt, ...) CountlyInternalLog(CLYInternalLogLevelError, fmt, ##__VA_ARGS__) #define CLY_LOG_W(fmt, ...) CountlyInternalLog(CLYInternalLogLevelWarning, fmt, ##__VA_ARGS__) diff --git a/CountlyConfig.h b/CountlyConfig.h index fa60993d..ce87ffaf 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -114,6 +114,15 @@ typedef void (^RCVariantCallback)(CLYRequestResult response, NSError *_Nullable typedef void (^RCDownloadCallback)(CLYRequestResult response, NSError *_Nullable error, BOOL fullValueUpdate, NSDictionary* downloadedValues); +typedef enum : NSUInteger +{ + COMPLETED, + CLOSED, +} ContentStatus; + +typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary* contentData); + + //NOTE: Internal log levels typedef enum : NSUInteger { @@ -661,6 +670,17 @@ typedef enum : NSUInteger * This will enable to fetch content from server and display them in UI */ @property (nonatomic) BOOL enableContentUpdates; + + /** + * Register global completion blocks to be executed content. + */ +- (void)setGlobalContentCallback:(ContentCallback) callback; + +/** + * Get content callback + */ +- (ContentCallback) getGlobalContentCallback; + NS_ASSUME_NONNULL_END @end diff --git a/CountlyConfig.m b/CountlyConfig.m index 5956f790..eefda253 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -8,6 +8,7 @@ @interface CountlyConfig () @property (nonatomic) NSMutableArray *remoteConfigGlobalCallbacks; +@property (nonatomic) ContentCallback contentCallback; @end @interface CountlyAPMConfig () @@ -124,4 +125,14 @@ - (nonnull CountlyCrashesConfig *)crashes { return crashes; } +-(void)setGlobalContentCallback:(ContentCallback) callback +{ + self.contentCallback = callback; +} + +- (ContentCallback) getGlobalContentCallback +{ + return self.contentCallback; +} + @end diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 4b4bda41..ca40f94a 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -6,6 +6,7 @@ #import #import +#import "CountlyCommon.h" @interface CountlyContentBuilderInternal: NSObject @@ -14,6 +15,7 @@ @property (nonatomic, assign) BOOL isContentConsentGiven; @property (nonatomic, assign) CGFloat density; @property (nonatomic, assign) NSTimeInterval requestInterval; +@property (nonatomic) ContentCallback contentCallback; + (instancetype)sharedInstance; diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index bcefe908..0eb00de3 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -4,7 +4,6 @@ // // Please visit www.count.ly for more information. #import "CountlyContentBuilderInternal.h" -#import "CountlyCommon.h" #import "CountlyWebViewManager.h" @@ -241,6 +240,9 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD } dismissBlock:^ { CLY_LOG_I(@"Webview dismissed"); + if(self.contentCallback) { + self.contentCallback(CLOSED, NSDictionary.new); + } }]; }); } From 3f4c15e26484e9beaf63ca54406e026c548b066a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 19 Aug 2024 18:15:48 +0500 Subject: [PATCH 034/148] Removed 'enableContentUpdates' from config level --- Countly.m | 10 ---------- CountlyConfig.h | 5 ----- 2 files changed, 15 deletions(-) diff --git a/Countly.m b/Countly.m index e6f55fb0..7fbbd0ed 100644 --- a/Countly.m +++ b/Countly.m @@ -268,16 +268,6 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyContentBuilderInternal.sharedInstance.contentCallback = config.getGlobalContentCallback; } - // Delay in seconds - double delayInSeconds = 1.0; // Adjust the delay time as needed - - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); - dispatch_after(delay, dispatch_get_main_queue(), ^{ - if(config.enableContentUpdates) { - [[self content] openForContent]; - } - }); - [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; diff --git a/CountlyConfig.h b/CountlyConfig.h index ce87ffaf..e3dd148d 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -666,11 +666,6 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL enableServerConfiguration; -/** - * This will enable to fetch content from server and display them in UI - */ -@property (nonatomic) BOOL enableContentUpdates; - /** * Register global completion blocks to be executed content. */ From e8a174e9b3c15c05810721e215bb4fc0fca6b872 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 12:51:21 +0500 Subject: [PATCH 035/148] Implemented new webview and content builder changes related to API's and url --- CountlyContentBuilderInternal.m | 15 ++--- CountlyWebViewManager.m | 100 +++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 0eb00de3..b6c53bd5 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -7,8 +7,8 @@ #import "CountlyWebViewManager.h" -NSString* const kCountlyEndpointContent = @"/i/content/queue"; //TODO: @"/content"; -NSString* const kCountlyCBFetchContent = @"fetch_content"; +NSString* const kCountlyEndpointContent = @"/o/sdk/content"; +NSString* const kCountlyCBFetchContent = @"queue"; NSString* const kCountlyCBCheckAvailbleContents = @"check_available_contents"; @implementation CountlyContentBuilderInternal { @@ -167,7 +167,7 @@ - (NSURLRequest *)fetchContentDetailsRequest:(NSString *)content_id queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlyCBFetchContent, @"content_id", content_id, - @"resolution", resolutionJson]; + @"res", resolutionJson]; queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; @@ -196,12 +196,13 @@ - (NSString *)resolutionJson { screenBounds.origin.y += 20.0; screenBounds.size.height -= 20.0; } - CGFloat width = screenBounds.size.width / self.density; - CGFloat height = screenBounds.size.height / self.density; + + CGFloat width = screenBounds.size.width; + CGFloat height = screenBounds.size.height; NSDictionary *resolutionDict = @{ - @"landscape": @{@"width": @(height), @"height": @(width)}, - @"portrait": @{@"width": @(width), @"height": @(height)} + @"p": @{@"h": @(height), @"w": @(width)}, + @"l": @{@"h": @(width), @"w": @(height)} }; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resolutionDict options:0 error:nil]; diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index d91e94e3..f5754f12 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -89,20 +89,25 @@ - (void)configureDismissButton:(CLYButton *)dismissButton forWebView:(WKWebView - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *url = navigationAction.request.URL.absoluteString; - if ([url containsString:@"cly_x_int=1"]) { - CLY_LOG_I(@"%s Opening URL [%@] in external browser", __FUNCTION__, url); - [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { - CLY_LOG_I(success ? @"%s URL [%@] opened in external browser" : @"%s Unable to open URL [%@] in external browser", __FUNCTION__, url); - }]; - decisionHandler(WKNavigationActionPolicyCancel); - } else if ([url containsString:@"cly_x_close=1"]) { - CLY_LOG_I(@"%s Closing webview", __FUNCTION__); - if (self.dismissBlock) { - dispatch_async(dispatch_get_main_queue(), ^{ - self.dismissBlock(); - [self.backgroundView removeFromSuperview]; - }); + if ([url containsString:@"cly_x_action_event=1"]) { + NSDictionary *queryParameters = [self parseQueryString:url]; + NSString *action = queryParameters[@"action"]; + + if ([action isEqualToString:@"event"]) { + NSString *event = queryParameters[@"event"]; + [self recordEventWithJSONString:event]; + } else if ([action isEqualToString:@"link"]) { + NSString *link = queryParameters[@"link"]; + [self openExternalLink:link]; + } else if ([action isEqualToString:@"resize_me"]) { + NSString *resize = queryParameters[@"resize_me"]; + [self resizeWebViewWithJSONString:resize]; + } + + if ([queryParameters[@"close"] boolValue]) { + [self closeWebView]; } + decisionHandler(WKNavigationActionPolicyCancel); } else { decisionHandler(WKNavigationActionPolicyAllow); @@ -152,7 +157,7 @@ - (void)animateView:(UIView *)view withAnimationType:(AnimationType)animationTyp }]; } -CGSize sizeForWebViewSize(WebViewSize size, CGRect backgroundFrame, UIEdgeInsets padding) { +- (CGSize)sizeForWebViewSize:(WebViewSize)size backgroundFrame:(CGRect)backgroundFrame padding:(UIEdgeInsets)padding { CGFloat width = backgroundFrame.size.width - padding.left - padding.right; CGFloat height = backgroundFrame.size.height - padding.top - padding.bottom; @@ -169,4 +174,71 @@ CGSize sizeForWebViewSize(WebViewSize size, CGRect backgroundFrame, UIEdgeInsets return CGSizeMake(width, height); } } + +- (NSDictionary *)parseQueryString:(NSString *)url { + NSMutableDictionary *queryDict = [NSMutableDictionary dictionary]; + NSArray *urlComponents = [url componentsSeparatedByString:@"?"]; + + if (urlComponents.count > 1) { + NSArray *queryItems = [urlComponents[1] componentsSeparatedByString:@"&"]; + + for (NSString *item in queryItems) { + NSArray *keyValue = [item componentsSeparatedByString:@"="]; + if (keyValue.count == 2) { + NSString *key = keyValue[0]; + NSString *value = keyValue[1]; + queryDict[key] = value; + } + } + } + + return queryDict; +} + +- (void)recordEventWithJSONString:(NSString *)jsonString { + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *eventDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + NSString *key = eventDict[@"key"]; + NSDictionary *segmentation = eventDict[@"sg"]; + + [Countly.sharedInstance recordEvent:key segmentation:segmentation]; +} + +- (void)openExternalLink:(NSString *)urlString { + NSURL *url = [NSURL URLWithString:urlString]; + if (url) { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) { + if (success) { + CLY_LOG_I(@"URL [%@] opened in external browser", urlString); + } else { + CLY_LOG_I(@"Unable to open URL [%@] in external browser", urlString); + } + }]; + } +} + +- (void)resizeWebViewWithJSONString:(NSString *)jsonString { + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *resizeDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + NSDictionary *portrait = resizeDict[@"p"]; + NSDictionary *landscape = resizeDict[@"l"]; + + CGFloat width = [portrait[@"w"] floatValue]; + CGFloat height = [portrait[@"h"] floatValue]; + + // Assuming you are resizing the web view here + CGRect newFrame = CGRectMake(self.backgroundView.webView.frame.origin.x, self.backgroundView.webView.frame.origin.y, width, height); + self.backgroundView.webView.frame = newFrame; +} + +- (void)closeWebView { + if (self.dismissBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.dismissBlock(); + [self.backgroundView removeFromSuperview]; + }); + } +} @end From 1ea39c4f6aab46ff6783774ac799372273bda811 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 13:03:33 +0500 Subject: [PATCH 036/148] Implemented consent for content builder --- CountlyConfig.h | 1 + CountlyConsentManager.h | 2 ++ CountlyConsentManager.m | 35 ++++++++++++++++++++++++++++++++- CountlyContentBuilderInternal.m | 6 ++++-- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CountlyConfig.h b/CountlyConfig.h index e3dd148d..583d1a65 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -77,6 +77,7 @@ extern CLYConsent const CLYConsentAttribution; extern CLYConsent const CLYConsentPerformanceMonitoring; extern CLYConsent const CLYConsentFeedback; extern CLYConsent const CLYConsentRemoteConfig; +extern CLYConsent const CLYConsentContent; //NOTE: Push Notification Test Modes typedef NSString* CLYPushTestMode NS_EXTENSIBLE_STRING_ENUM; diff --git a/CountlyConsentManager.h b/CountlyConsentManager.h index 5846a636..4bab37d0 100644 --- a/CountlyConsentManager.h +++ b/CountlyConsentManager.h @@ -22,6 +22,8 @@ @property (nonatomic, readonly) BOOL consentForPerformanceMonitoring; @property (nonatomic, readonly) BOOL consentForFeedback; @property (nonatomic, readonly) BOOL consentForRemoteConfig; +@property (nonatomic, readonly) BOOL consentForContent; + + (instancetype)sharedInstance; - (void)giveConsentForFeatures:(NSArray *)features; diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 6299f623..827c9517 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -17,6 +17,7 @@ CLYConsent const CLYConsentPerformanceMonitoring = @"apm"; CLYConsent const CLYConsentFeedback = @"feedback"; CLYConsent const CLYConsentRemoteConfig = @"remote-config"; +CLYConsent const CLYConsentContent = @"content"; @implementation CountlyConsentManager @@ -32,6 +33,7 @@ @implementation CountlyConsentManager @synthesize consentForPerformanceMonitoring = _consentForPerformanceMonitoring; @synthesize consentForFeedback = _consentForFeedback; @synthesize consentForRemoteConfig = _consentForRemoteConfig; +@synthesize consentForContent = _consentForContent; #pragma mark - @@ -114,6 +116,9 @@ - (void)giveConsentForFeatures:(NSArray *)features if ([features containsObject:CLYConsentRemoteConfig] && !self.consentForRemoteConfig) self.consentForRemoteConfig = YES; + + if ([features containsObject:CLYConsentContent] && !self.consentForContent) + self.consentForContent = YES; [self sendConsents]; } @@ -177,6 +182,9 @@ - (void)cancelConsentForFeatures:(NSArray *)features shouldSkipSendingConsentsRe if ([features containsObject:CLYConsentRemoteConfig] && self.consentForRemoteConfig) self.consentForRemoteConfig = NO; + + if ([features containsObject:CLYConsentContent] && self.consentForContent) + self.consentForContent = NO; if (!shouldSkipSendingConsentsRequest) [self sendConsents]; @@ -198,6 +206,7 @@ - (void)sendConsents CLYConsentPerformanceMonitoring: @(self.consentForPerformanceMonitoring), CLYConsentFeedback: @(self.consentForFeedback), CLYConsentRemoteConfig: @(self.consentForRemoteConfig), + CLYConsentContent: @(self.consentForContent), }; [CountlyConnectionManager.sharedInstance sendConsents:[consents cly_JSONify]]; @@ -236,7 +245,8 @@ - (BOOL)hasAnyConsent self.consentForAttribution || self.consentForPerformanceMonitoring || self.consentForFeedback || - self.consentForRemoteConfig; + self.consentForRemoteConfig || + self.consentForContent; } @@ -452,6 +462,21 @@ - (void)setConsentForRemoteConfig:(BOOL)consentForRemoteConfig } } +- (void)setConsentForContent:(BOOL)consentForContent +{ + _consentForContent = consentForContent; + + if (consentForContent) + { + CLY_LOG_D(@"Consent for Content is given."); + } + else + { + CLY_LOG_D(@"Consent for Content is cancelled."); + [CountlyContentBuilderInternal.sharedInstance exitFromContent]; + } +} + #pragma mark - - (BOOL)consentForSessions @@ -550,4 +575,12 @@ - (BOOL)consentForRemoteConfig return _consentForRemoteConfig; } +- (BOOL)consentForContent +{ + if (!self.requiresConsent) + return YES; + + return _consentForContent; +} + @end diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index b6c53bd5..03d87c69 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -40,9 +40,8 @@ - (instancetype)init } - (void)openForContent:(NSArray *)tags { - if (!self.isContentConsentGiven) { + if (!CountlyConsentManager.sharedInstance.consentForContent) return; - } if(_requestTimer != nil) { CLY_LOG_I(@"Already open for content, please exit from content first to start again"); @@ -116,6 +115,9 @@ - (void) fetchContentDetailsForContentId { } - (void)fetchContentDetailsForContentId:(NSString *)contentId { //TODO: removed _isRequestQueueLocked from this method when we are using 'sendContentCheckRequest' + if (!CountlyConsentManager.sharedInstance.consentForContent) + return; + if (_isRequestQueueLocked) { return; } From 9bf177ff7a1e356d2854da8391867c6edec42a5a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 13:08:44 +0500 Subject: [PATCH 037/148] Cleanup content builder code --- CountlyContentBuilderInternal.h | 3 -- CountlyContentBuilderInternal.m | 83 ++++----------------------------- 2 files changed, 10 insertions(+), 76 deletions(-) diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index ca40f94a..11ebc767 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -11,9 +11,6 @@ @interface CountlyContentBuilderInternal: NSObject @property (nonatomic, strong) NSArray *currentTags; -@property (nonatomic, strong) NSString *latestChecksum; -@property (nonatomic, assign) BOOL isContentConsentGiven; -@property (nonatomic, assign) CGFloat density; @property (nonatomic, assign) NSTimeInterval requestInterval; @property (nonatomic) ContentCallback contentCallback; diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 03d87c69..daa6d4ef 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -9,7 +9,6 @@ NSString* const kCountlyEndpointContent = @"/o/sdk/content"; NSString* const kCountlyCBFetchContent = @"queue"; -NSString* const kCountlyCBCheckAvailbleContents = @"check_available_contents"; @implementation CountlyContentBuilderInternal { BOOL _isRequestQueueLocked; @@ -29,9 +28,6 @@ - (instancetype)init { if (self = [super init]) { - self.isContentConsentGiven = YES; - self.latestChecksum = nil; - self.density = 1; //TODO: [UIScreen mainScreen].scale; self.requestInterval = 30.0; _requestTimer = nil; } @@ -49,10 +45,10 @@ - (void)openForContent:(NSArray *)tags { } self.currentTags = tags; - [self fetchContentDetailsForContentId:@"contentId"]; //TODO: [self sendContentCheckRequest]; + [self fetchContents];; _requestTimer = [NSTimer scheduledTimerWithTimeInterval:self.requestInterval target:self - selector:@selector(fetchContentDetailsForContentId) // TODO: sendContentCheckRequest + selector:@selector(fetchContents) userInfo:nil repeats:YES]; } @@ -74,47 +70,10 @@ - (void)clearContentState { [_requestTimer invalidate]; _requestTimer = nil; self.currentTags = nil; - self.latestChecksum = nil; _isRequestQueueLocked = NO; } -- (void)sendContentCheckRequest { - if (!self.isContentConsentGiven || _isRequestQueueLocked) { - return; - } - - _isRequestQueueLocked = YES; - - // Send request - NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:[self checkContentRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - CLY_LOG_I(@"Content check request failed: %@", error); - self->_isRequestQueueLocked = NO; - return; - } - - // Process the response - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *newChecksum = jsonResponse[@"checksum"]; - NSString *contentId = jsonResponse[@"content_id"]; - - if (newChecksum && ![newChecksum isEqualToString:self.latestChecksum]) { - self.latestChecksum = newChecksum; - [self fetchContentDetailsForContentId:contentId]; - } - - self->_isRequestQueueLocked = NO; - }]; - - [task resume]; -} - -//TODO: remove this method -- (void) fetchContentDetailsForContentId { - [self fetchContentDetailsForContentId:@""]; -} -- (void)fetchContentDetailsForContentId:(NSString *)contentId { - //TODO: removed _isRequestQueueLocked from this method when we are using 'sendContentCheckRequest' +- (void)fetchContents { if (!CountlyConsentManager.sharedInstance.consentForContent) return; @@ -124,7 +83,7 @@ - (void)fetchContentDetailsForContentId:(NSString *)contentId { _isRequestQueueLocked = YES; - NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:[self fetchContentDetailsRequest:contentId] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:[self fetchContentsRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { CLY_LOG_I(@"Fetch content details failed: %@", error); self->_isRequestQueueLocked = NO; @@ -142,33 +101,12 @@ - (void)fetchContentDetailsForContentId:(NSString *)contentId { [dataTask resume]; } -- (NSURLRequest *)checkContentRequest -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - NSString *tagsString = [self.currentTags componentsJoinedByString:@","]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=[%@]", - kCountlyQSKeyMethod, kCountlyCBCheckAvailbleContents, - @"tags", tagsString]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", - CountlyConnectionManager.sharedInstance.host, - kCountlyEndpointContent, - queryString]; - - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]; - return request; -} - -- (NSURLRequest *)fetchContentDetailsRequest:(NSString *)content_id +- (NSURLRequest *)fetchContentsRequest { NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; NSString *resolutionJson = [self resolutionJson]; - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%@", + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlyCBFetchContent, - @"content_id", content_id, @"res", resolutionJson]; queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; @@ -224,11 +162,10 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD // Get the appropriate coordinates based on the orientation NSDictionary *coordinates = isLandscape ? placementCoordinates[@"landscape"] : placementCoordinates[@"portrait"]; - // Extract placement coordinates and adjust for screen density - CGFloat x = [coordinates[@"x"] floatValue] / self.density; - CGFloat y = [coordinates[@"y"] floatValue] / self.density; - CGFloat width = [coordinates[@"width"] floatValue] / self.density; - CGFloat height = [coordinates[@"height"] floatValue] / self.density; + CGFloat x = [coordinates[@"x"] floatValue]; + CGFloat y = [coordinates[@"y"] floatValue]; + CGFloat width = [coordinates[@"width"] floatValue]; + CGFloat height = [coordinates[@"height"] floatValue]; CGRect frame = CGRectMake(x, y, width, height); From 9b02df4a4c9d7e4f6a093b30e6de3bc4f9b456b1 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 13:17:30 +0500 Subject: [PATCH 038/148] Cleanup webview manager code --- CountlyWebViewManager.h | 17 ------------ CountlyWebViewManager.m | 61 +++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index 51edbea1..3edf17af 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -7,23 +7,6 @@ #import #import -typedef NS_ENUM(NSInteger, WebViewPosition) { - WebViewPositionTop, - WebViewPositionBottom, - WebViewPositionCenter, - WebViewPositionTopLeft, - WebViewPositionTopRight, - WebViewPositionBottomLeft, - WebViewPositionBottomRight, -}; - -typedef NS_ENUM(NSInteger, WebViewSize) { - WebViewFullScreen, - WebViewHalf, - WebViewBanner, - WebViewSquareSmall -}; - typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeSlideInFromBottom, AnimationTypeSlideInFromTop, diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index f5754f12..e71bd549 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -94,8 +94,8 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati NSString *action = queryParameters[@"action"]; if ([action isEqualToString:@"event"]) { - NSString *event = queryParameters[@"event"]; - [self recordEventWithJSONString:event]; + NSString *eventsJson = queryParameters[@"event"]; + [self recordEventsWithJSONString:eventsJson]; } else if ([action isEqualToString:@"link"]) { NSString *link = queryParameters[@"link"]; [self openExternalLink:link]; @@ -157,24 +157,6 @@ - (void)animateView:(UIView *)view withAnimationType:(AnimationType)animationTyp }]; } -- (CGSize)sizeForWebViewSize:(WebViewSize)size backgroundFrame:(CGRect)backgroundFrame padding:(UIEdgeInsets)padding { - CGFloat width = backgroundFrame.size.width - padding.left - padding.right; - CGFloat height = backgroundFrame.size.height - padding.top - padding.bottom; - - switch (size) { - case WebViewFullScreen: - return CGSizeMake(width, height); - case WebViewHalf: - return CGSizeMake(width, height / 2); - case WebViewBanner: - return CGSizeMake(width, 70); - case WebViewSquareSmall: - return CGSizeMake(250, 250); - default: - return CGSizeMake(width, height); - } -} - - (NSDictionary *)parseQueryString:(NSString *)url { NSMutableDictionary *queryDict = [NSMutableDictionary dictionary]; NSArray *urlComponents = [url componentsSeparatedByString:@"?"]; @@ -195,14 +177,16 @@ - (NSDictionary *)parseQueryString:(NSString *)url { return queryDict; } -- (void)recordEventWithJSONString:(NSString *)jsonString { +- (void)recordEventsWithJSONString:(NSString *)jsonString { NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *eventDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - - NSString *key = eventDict[@"key"]; - NSDictionary *segmentation = eventDict[@"sg"]; + NSArray *events = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - [Countly.sharedInstance recordEvent:key segmentation:segmentation]; + for (NSDictionary *event in events) { + NSString *key = event[@"key"]; + NSDictionary *segmentation = event[@"sg"]; + + [Countly.sharedInstance recordEvent:key segmentation:segmentation]; + } } - (void)openExternalLink:(NSString *)urlString { @@ -222,17 +206,28 @@ - (void)resizeWebViewWithJSONString:(NSString *)jsonString { NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *resizeDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSDictionary *portrait = resizeDict[@"p"]; - NSDictionary *landscape = resizeDict[@"l"]; + NSDictionary *portraitDimensions = resizeDict[@"p"]; + NSDictionary *landscapeDimensions = resizeDict[@"l"]; + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); + + NSDictionary *dimensions = isLandscape ? landscapeDimensions : portraitDimensions; - CGFloat width = [portrait[@"w"] floatValue]; - CGFloat height = [portrait[@"h"] floatValue]; + CGFloat width = [dimensions[@"w"] floatValue]; + CGFloat height = [dimensions[@"h"] floatValue]; - // Assuming you are resizing the web view here - CGRect newFrame = CGRectMake(self.backgroundView.webView.frame.origin.x, self.backgroundView.webView.frame.origin.y, width, height); - self.backgroundView.webView.frame = newFrame; + [UIView animateWithDuration:0.3 animations:^{ + CGRect frame = self.backgroundView.webView.frame; + frame.size.width = width; + frame.size.height = height; + self.backgroundView.webView.frame = frame; + } completion:^(BOOL finished) { + CLY_LOG_I(@"Resized web view to width: %f, height: %f", width, height); + }]; } + - (void)closeWebView { if (self.dismissBlock) { dispatch_async(dispatch_get_main_queue(), ^{ From 2cfad9f5315362908b0bac6ddad4697b830278ce Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 17:32:59 +0500 Subject: [PATCH 039/148] added small changes --- CountlyContentBuilderInternal.m | 1 + CountlyWebViewManager.m | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index daa6d4ef..71d4b0d1 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -121,6 +121,7 @@ - (NSURLRequest *)fetchContentsRequest } - (NSString *)resolutionJson { + //TODO: check why area is not clickable and safearea things CGRect screenBounds = [UIScreen mainScreen].bounds; if (@available(iOS 11.0, *)) { CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index e71bd549..a61ea8c1 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -89,7 +89,7 @@ - (void)configureDismissButton:(CLYButton *)dismissButton forWebView:(WKWebView - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *url = navigationAction.request.URL.absoluteString; - if ([url containsString:@"cly_x_action_event=1"]) { + if ([url hasPrefix:@"https://countly_action_event"] && [url containsString:@"cly_x_action_event=1"]) { NSDictionary *queryParameters = [self parseQueryString:url]; NSString *action = queryParameters[@"action"]; @@ -184,6 +184,10 @@ - (void)recordEventsWithJSONString:(NSString *)jsonString { for (NSDictionary *event in events) { NSString *key = event[@"key"]; NSDictionary *segmentation = event[@"sg"]; + if(!segmentation) { + CLY_LOG_I(@"Skipping the event due to missing segmentation"); + continue; + } [Countly.sharedInstance recordEvent:key segmentation:segmentation]; } @@ -229,11 +233,11 @@ - (void)resizeWebViewWithJSONString:(NSString *)jsonString { - (void)closeWebView { - if (self.dismissBlock) { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.dismissBlock) { self.dismissBlock(); - [self.backgroundView removeFromSuperview]; - }); - } + } + [self.backgroundView removeFromSuperview]; + }); } @end From 4905a58c3812eb165b191c67ce9617c838a653c9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 18:46:55 +0500 Subject: [PATCH 040/148] Added todos for logging --- CountlyContentBuilderInternal.m | 2 +- CountlyWebViewManager.m | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 71d4b0d1..7dc0313a 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -6,7 +6,7 @@ #import "CountlyContentBuilderInternal.h" #import "CountlyWebViewManager.h" - +//TODO: improve logging, check edge cases NSString* const kCountlyEndpointContent = @"/o/sdk/content"; NSString* const kCountlyCBFetchContent = @"queue"; diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index a61ea8c1..8e9e85ad 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -2,6 +2,7 @@ #import "PassThroughBackgroundView.h" #import "CountlyCommon.h" +//TODO: improve logging, check edge cases @interface CountlyWebViewManager() @property (nonatomic, strong) PassThroughBackgroundView *backgroundView; @property (nonatomic, copy) void (^dismissBlock)(void); From 93ddca95e72ae1847c5d530775ff8a2b8e99b909 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 18:49:13 +0500 Subject: [PATCH 041/148] Revert "Removed start view segmentation caching" This reverts commit 1cfb5f0fc71fa58c7557e8b2522c09bc059c860c. --- CountlyViewData.h | 6 ++++++ CountlyViewTrackingInternal.m | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CountlyViewData.h b/CountlyViewData.h index 04c883dc..d56dc687 100644 --- a/CountlyViewData.h +++ b/CountlyViewData.h @@ -42,6 +42,12 @@ */ @property (nonatomic) NSMutableDictionary* segmentation; +/** + * Segmentation of start view . + * @discussion This segmentation will store to send again when view is start again when app goes to foreground + */ +@property (nonatomic) NSMutableDictionary* startSegmentation; + /** * Initialize view data * @discussion If set then this view will automatically stopped when new view is started. diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 30bf7eb9..46c77f57 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -441,6 +441,7 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio self.currentViewID = CountlyCommon.sharedInstance.randomEventID; CountlyViewData *viewData = [[CountlyViewData alloc] initWithID:self.currentViewID viewName:viewName]; + viewData.startSegmentation = customSegmentation.mutableCopy; viewData.isAutoStoppedView = isAutoStoppedView; self.viewDataDictionary[self.currentViewID] = viewData; @@ -540,7 +541,7 @@ - (void)startStoppedViewsInternal [self.viewDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CountlyViewData * _Nonnull viewData, BOOL * _Nonnull stop) { if (viewData.willStartAgain) { - NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:nil isAutoStoppedView:viewData.isAutoStoppedView]; + NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:viewData.startSegmentation isAutoStoppedView:viewData.isAutoStoppedView]; // Add the old view's ID to the array for removal later [keysToRemove addObject:viewData.viewID]; From 518134706b8fa5fdf4cd24ea3d3f5899fd2556b3 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 21 Aug 2024 18:49:22 +0500 Subject: [PATCH 042/148] Revert "No need to send old view segmentation when starting a view again" This reverts commit 43c018f228b834baa882257f25ceaa33732a3898. --- CountlyViewTrackingInternal.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 46c77f57..f2366c94 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -543,6 +543,12 @@ - (void)startStoppedViewsInternal { NSString *viewID = [self startViewInternal:viewData.viewName customSegmentation:viewData.startSegmentation isAutoStoppedView:viewData.isAutoStoppedView]; + // Retrieve the newly created viewData for the viewID + CountlyViewData* viewDataNew = self.viewDataDictionary[viewID]; + + // Copy the segmentation data from the old view to the new view + viewDataNew.segmentation = viewData.segmentation.mutableCopy; + // Add the old view's ID to the array for removal later [keysToRemove addObject:viewData.viewID]; } From 67ec6cc9a71b98ba51bc9dbe8fe8b150184fc236 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 29 Aug 2024 11:43:40 +0500 Subject: [PATCH 043/148] Added iOS macros checks on content builder and Custom web code --- Countly.h | 13 +++++++------ Countly.m | 13 +++++++------ CountlyConfig.h | 5 ++++- CountlyConfig.m | 4 ++++ CountlyConsentManager.m | 2 ++ CountlyContentBuilder.h | 2 ++ CountlyContentBuilder.m | 2 ++ CountlyContentBuilderInternal.h | 2 ++ CountlyContentBuilderInternal.m | 2 ++ CountlyWebViewManager.h | 2 ++ CountlyWebViewManager.m | 3 +++ PassThroughBackgroundView.h | 2 ++ PassThroughBackgroundView.m | 2 ++ 13 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Countly.h b/Countly.h index d702d703..94a33d09 100644 --- a/Countly.h +++ b/Countly.h @@ -678,6 +678,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)getFeedbackWidgets:(void (^)(NSArray * __nullable feedbackWidgets, NSError * __nullable error))completionHandler; + +/** + * Interface variable to access views tracking functionalities. + * @discussion Views tracking interface for developer to interact with SDK. + */ +- (CountlyContentBuilder *_Nonnull) content; + #endif @@ -830,10 +837,4 @@ NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END -/** - * Interface variable to access views tracking functionalities. - * @discussion Views tracking interface for developer to interact with SDK. - */ -- (CountlyContentBuilder *_Nonnull) content; - @end diff --git a/Countly.m b/Countly.m index 7fbbd0ed..6e6c8561 100644 --- a/Countly.m +++ b/Countly.m @@ -263,10 +263,11 @@ - (void)startWithConfig:(CountlyConfig *)config if (config.apm.getAppStartTimestampOverride) { appLoadStartTime = config.apm.getAppStartTimestampOverride; } - +#if (TARGET_OS_IOS) if(config.getGlobalContentCallback) { CountlyContentBuilderInternal.sharedInstance.contentCallback = config.getGlobalContentCallback; } +#endif [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; @@ -1233,6 +1234,11 @@ - (void)getFeedbackWidgets:(void (^)(NSArray *feedback [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; } +- (CountlyContentBuilder *) content +{ + return CountlyContentBuilder.sharedInstance; +} + #endif @@ -1436,9 +1442,4 @@ - (void)attemptToSendStoredRequests [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; } -- (CountlyContentBuilder *) content -{ - return CountlyContentBuilder.sharedInstance; -} - @end diff --git a/CountlyConfig.h b/CountlyConfig.h index 583d1a65..144210ab 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -115,6 +115,7 @@ typedef void (^RCVariantCallback)(CLYRequestResult response, NSError *_Nullable typedef void (^RCDownloadCallback)(CLYRequestResult response, NSError *_Nullable error, BOOL fullValueUpdate, NSDictionary* downloadedValues); +#if (TARGET_OS_IOS) typedef enum : NSUInteger { COMPLETED, @@ -122,7 +123,7 @@ typedef enum : NSUInteger } ContentStatus; typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary* contentData); - +#endif //NOTE: Internal log levels typedef enum : NSUInteger @@ -667,6 +668,7 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL enableServerConfiguration; +#if (TARGET_OS_IOS) /** * Register global completion blocks to be executed content. */ @@ -676,6 +678,7 @@ typedef enum : NSUInteger * Get content callback */ - (ContentCallback) getGlobalContentCallback; +#endif NS_ASSUME_NONNULL_END diff --git a/CountlyConfig.m b/CountlyConfig.m index eefda253..5fafa158 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -8,7 +8,9 @@ @interface CountlyConfig () @property (nonatomic) NSMutableArray *remoteConfigGlobalCallbacks; +#if (TARGET_OS_IOS) @property (nonatomic) ContentCallback contentCallback; +#endif @end @interface CountlyAPMConfig () @@ -125,6 +127,7 @@ - (nonnull CountlyCrashesConfig *)crashes { return crashes; } +#if (TARGET_OS_IOS) -(void)setGlobalContentCallback:(ContentCallback) callback { self.contentCallback = callback; @@ -134,5 +137,6 @@ - (ContentCallback) getGlobalContentCallback { return self.contentCallback; } +#endif @end diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 827c9517..9f01acf8 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -473,7 +473,9 @@ - (void)setConsentForContent:(BOOL)consentForContent else { CLY_LOG_D(@"Consent for Content is cancelled."); +#if (TARGET_OS_IOS) [CountlyContentBuilderInternal.sharedInstance exitFromContent]; +#endif } } diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 710c7334..ee18e76f 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import #import @@ -17,3 +18,4 @@ - (void)changeContent:(NSArray *)tags; @end +#endif diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index 76692984..b5ca6b8f 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import "CountlyContentBuilder.h" #import "CountlyContentBuilderInternal.h" #import "CountlyCommon.h" @@ -47,3 +48,4 @@ - (void)changeContent:(NSArray *)tags } @end +#endif diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 11ebc767..3620316d 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import #import #import "CountlyCommon.h" @@ -21,4 +22,5 @@ - (void)changeContent:(NSArray *)tags; @end +#endif diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 7dc0313a..66ab9ca9 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -3,6 +3,7 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import "CountlyContentBuilderInternal.h" #import "CountlyWebViewManager.h" @@ -188,3 +189,4 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD }); } @end +#endif diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index 3edf17af..c2d09617 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import #import @@ -25,3 +26,4 @@ typedef NS_ENUM(NSUInteger, AnimationType) { dismissBlock:(void(^ __nullable)(void))dismissBlock; @end +#endif diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index 8e9e85ad..1ae3d71f 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -1,3 +1,5 @@ + +#if (TARGET_OS_IOS) #import "CountlyWebViewManager.h" #import "PassThroughBackgroundView.h" #import "CountlyCommon.h" @@ -242,3 +244,4 @@ - (void)closeWebView { }); } @end +#endif diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h index 76614706..722cb1ba 100644 --- a/PassThroughBackgroundView.h +++ b/PassThroughBackgroundView.h @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import #import #import "CountlyCommon.h" @@ -15,3 +16,4 @@ @end +#endif diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m index 2ba419b4..b9af0370 100644 --- a/PassThroughBackgroundView.m +++ b/PassThroughBackgroundView.m @@ -4,6 +4,7 @@ // // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import "PassThroughBackgroundView.h" @implementation PassThroughBackgroundView @@ -30,3 +31,4 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { } @end +#endif From 3b0d6eb2095e5acf70a59ddffa3aa1fa57321fb2 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 29 Aug 2024 19:05:07 +0500 Subject: [PATCH 044/148] Implemented app visibility and current/previous view name for events segmentation --- Countly.m | 46 +++++++++++++++++++++++++++---- Countly.xcodeproj/project.pbxproj | 6 ++++ CountlyCommon.h | 7 ++++- CountlyCommon.m | 5 +++- CountlyConfig.h | 7 +++++ CountlyConfig.m | 8 ++++++ CountlyExperimentalConfig.h | 16 +++++++++++ CountlyExperimentalConfig.m | 22 +++++++++++++++ CountlyViewTrackingInternal.h | 7 +++++ CountlyViewTrackingInternal.m | 3 ++ 10 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 CountlyExperimentalConfig.h create mode 100644 CountlyExperimentalConfig.m diff --git a/Countly.m b/Countly.m index de11d7bd..62c86830 100644 --- a/Countly.m +++ b/Countly.m @@ -244,6 +244,13 @@ - (void)startWithConfig:(CountlyConfig *)config [CountlyViewTrackingInternal.sharedInstance addAutoViewTrackingExclutionList:config.automaticViewTrackingExclusionList]; } #endif + + if(config.experimental.enableViewNameRecording) { + CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording = YES; + } + if(config.experimental.enableVisibiltyTracking) { + CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; + } if (config.globalViewSegmentation) { [CountlyViewTrackingInternal.sharedInstance setGlobalViewSegmentation:config.globalViewSegmentation]; } @@ -879,11 +886,9 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co event.ID = CountlyCommon.sharedInstance.randomEventID; } - if ([key isEqualToString:kCountlyReservedEventView]) - { - event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; - } - else + event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; + + if(![key isEqualToString:kCountlyReservedEventView]) { event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; } @@ -899,7 +904,20 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co previousEventID = event.ID; } event.key = key; - event.segmentation = segmentation.cly_filterSupportedDataTypes; + NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; + if(CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording) { + if(CountlyViewTrackingInternal.sharedInstance.currentViewName && ![key isEqualToString:kCountlyReservedEventView]) { + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName; + } + if(CountlyViewTrackingInternal.sharedInstance.previousViewName) { + filteredSegmentations[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName; + } + } + + if(CountlyCommon.sharedInstance.enableVisibiltyTracking) { + filteredSegmentations[kCountlyVisibility] = @([self isAppInForeground]); + } + event.segmentation = filteredSegmentations; event.count = MAX(count, 1); event.sum = sum; event.timestamp = timestamp; @@ -910,6 +928,22 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co [CountlyPersistency.sharedInstance recordEvent:event]; } +- (BOOL)isAppInForeground { +#if TARGET_OS_IOS || TARGET_OS_TV + UIApplicationState state = [UIApplication sharedApplication].applicationState; + return state == UIApplicationStateActive; +#elif TARGET_OS_OSX + NSApplication *app = [NSApplication sharedApplication]; + return app.isActive; +#elif TARGET_OS_WATCH + WKExtension *extension = [WKExtension sharedExtension]; + return extension.applicationState == WKApplicationStateActive; +#else + return NO; +#endif +} + + - (BOOL)isReservedEvent:(NSString *)key { NSArray* reservedEvents = diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 1b9594ba..75ea3cfb 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 1ACA5DC12A309E7F001F770B /* CountlyRemoteConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACA5DBF2A309E7F001F770B /* CountlyRemoteConfigInternal.h */; }; 1ACA5DC22A309E7F001F770B /* CountlyRemoteConfigInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACA5DC02A309E7F001F770B /* CountlyRemoteConfigInternal.m */; }; 1AFD79022B3EF82C00772FBD /* CountlyTests-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */; }; + 3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */; }; 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3948A8552BAC2E7D002D09AA /* CountlySDKLimitsConfig.m */; }; 3948A8582BAC2E7D002D09AA /* CountlySDKLimitsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3948A8562BAC2E7D002D09AA /* CountlySDKLimitsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39527E152B5FD27400EE5D7B /* CountlyAPMConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39527E142B5FD27400EE5D7B /* CountlyAPMConfig.m */; }; @@ -104,6 +105,8 @@ 1ACA5DBF2A309E7F001F770B /* CountlyRemoteConfigInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyRemoteConfigInternal.h; sourceTree = ""; }; 1ACA5DC02A309E7F001F770B /* CountlyRemoteConfigInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyRemoteConfigInternal.m; sourceTree = ""; }; 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CountlyTests-Bridging-Header.h"; sourceTree = ""; }; + 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyExperimentalConfig.h; sourceTree = ""; }; + 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyExperimentalConfig.m; sourceTree = ""; }; 3948A8552BAC2E7D002D09AA /* CountlySDKLimitsConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlySDKLimitsConfig.m; sourceTree = ""; }; 3948A8562BAC2E7D002D09AA /* CountlySDKLimitsConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlySDKLimitsConfig.h; sourceTree = ""; }; 39527E142B5FD27400EE5D7B /* CountlyAPMConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyAPMConfig.m; sourceTree = ""; }; @@ -200,6 +203,8 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( + 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */, + 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */, 39924ED72BEBD22100139F91 /* CountlyCrashData.h */, 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */, 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */, @@ -455,6 +460,7 @@ 3B20A9C02245228700E3D7AE /* CountlyConsentManager.m in Sources */, 39527E152B5FD27400EE5D7B /* CountlyAPMConfig.m in Sources */, 1A3110712A7141AF001CB507 /* CountlyViewTracking.m in Sources */, + 3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */, 1A3A576329ED47A20041B7BE /* CountlyServerConfig.m in Sources */, D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */, 3B20A9B42245228700E3D7AE /* CountlyPushNotifications.m in Sources */, diff --git a/CountlyCommon.h b/CountlyCommon.h index 9eff5880..68fd7899 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -29,6 +29,7 @@ #import "CountlyViewTracking.h" #import "Resettable.h" #import "CountlyCrashData.h" +#import "CountlyExperimentalConfig.h" #define CLY_LOG_E(fmt, ...) CountlyInternalLog(CLYInternalLogLevelError, fmt, ##__VA_ARGS__) #define CLY_LOG_W(fmt, ...) CountlyInternalLog(CLYInternalLogLevelWarning, fmt, ##__VA_ARGS__) @@ -57,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString* const kCountlyErrorDomain; extern NSString* const kCountlyReservedEventOrientation; +extern NSString* const kCountlyVisibility; NS_ERROR_ENUM(kCountlyErrorDomain) { @@ -77,6 +79,7 @@ extern NSString* const kCountlySDKName; @property (nonatomic) BOOL hasStarted; @property (nonatomic) BOOL enableDebug; + @property (nonatomic) BOOL shouldIgnoreTrustCheck; @property (nonatomic, weak) id loggerDelegate; @property (nonatomic) CLYInternalLogLevel internalLogLevel; @@ -86,6 +89,8 @@ extern NSString* const kCountlySDKName; @property (nonatomic) BOOL enableOrientationTracking; @property (nonatomic) BOOL enableServerConfiguration; +@property (nonatomic) BOOL enableVisibiltyTracking; + @property (nonatomic) NSUInteger maxKeyLength; @property (nonatomic) NSUInteger maxValueLength; @@ -155,7 +160,7 @@ void CountlyPrint(NSString *stringToPrint); - (NSString *)cly_JSONify; - (NSDictionary *)cly_truncated:(NSString *)explanation; - (NSDictionary *)cly_limited:(NSString *)explanation; -- (NSDictionary *)cly_filterSupportedDataTypes; +- (NSMutableDictionary *)cly_filterSupportedDataTypes; @end @interface NSData (Countly) diff --git a/CountlyCommon.m b/CountlyCommon.m index c673562c..27e61095 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -10,6 +10,9 @@ NSString* const kCountlyReservedEventOrientation = @"[CLY]_orientation"; NSString* const kCountlyOrientationKeyMode = @"mode"; +NSString* const kCountlyVisibility = @"cly_v"; + + @interface CountlyCommon () { NSCalendar* gregorianCalendar; @@ -641,7 +644,7 @@ - (NSDictionary *)cly_limited:(NSString *)explanation return limitedDict.copy; } -- (NSDictionary *) cly_filterSupportedDataTypes +- (NSMutableDictionary *) cly_filterSupportedDataTypes { NSMutableDictionary *filteredDictionary = [NSMutableDictionary dictionary]; diff --git a/CountlyConfig.h b/CountlyConfig.h index fbc2e979..633ceeae 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -10,6 +10,7 @@ #import "CountlyAPMConfig.h" #import "CountlyCrashesConfig.h" #import "CountlySDKLimitsConfig.h" +#import "CountlyExperimentalConfig.h" #if (TARGET_OS_IOS || TARGET_OS_TV) #import @@ -656,6 +657,12 @@ typedef enum : NSUInteger * @discussion If set, Server Config values from Countly Server will be fetched at the beginning of a session. */ @property (nonatomic) BOOL enableServerConfiguration; + +/** + * Variable to access experimental configurations. + * @discussion Experimental configurations for developer to interact with SDK. + */ +- (CountlyExperimentalConfig *) experimental; NS_ASSUME_NONNULL_END @end diff --git a/CountlyConfig.m b/CountlyConfig.m index 5956f790..2e58c32a 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -36,6 +36,7 @@ @implementation CountlyConfig CountlyAPMConfig *apmConfig = nil; CountlyCrashesConfig *crashes = nil; CountlySDKLimitsConfig *sdkLimitsConfig = nil; +CountlyExperimentalConfig *experimental = nil; //NOTE: Device ID options NSString* const CLYDefaultDeviceID = @""; //NOTE: It will be overridden to default device ID mechanism, depending on platform. @@ -124,4 +125,11 @@ - (nonnull CountlyCrashesConfig *)crashes { return crashes; } +- (nonnull CountlyExperimentalConfig *)experimental { + if (experimental == nil) { + experimental = CountlyExperimentalConfig.new; + } + return experimental; +} + @end diff --git a/CountlyExperimentalConfig.h b/CountlyExperimentalConfig.h new file mode 100644 index 00000000..89644bcd --- /dev/null +++ b/CountlyExperimentalConfig.h @@ -0,0 +1,16 @@ +// CountlyExperimentalConfig.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +extern NSString* const kCountlySCKeySC; + +@interface CountlyExperimentalConfig : NSObject + +@property (nonatomic) BOOL enableViewNameRecording; +@property (nonatomic) BOOL enableVisibiltyTracking; + +@end diff --git a/CountlyExperimentalConfig.m b/CountlyExperimentalConfig.m new file mode 100644 index 00000000..41432ea0 --- /dev/null +++ b/CountlyExperimentalConfig.m @@ -0,0 +1,22 @@ +// CountlyExperimentalConfig.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyCommon.h" + +@implementation CountlyExperimentalConfig + +- (instancetype)init +{ + if (self = [super init]) + { + self.enableVisibiltyTracking = NO; + self.enableViewNameRecording = NO; + } + + return self; +} + +@end diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index a27e19c8..3c089493 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -8,11 +8,18 @@ extern NSString* const kCountlyReservedEventView; +extern NSString* const kCountlyCurrentView; +extern NSString* const kCountlyPreviousView; + @interface CountlyViewTrackingInternal : NSObject @property (nonatomic) BOOL isEnabledOnInitialConfig; @property (nonatomic) NSString* currentViewID; @property (nonatomic) NSString* previousViewID; +@property (nonatomic) BOOL enableViewNameRecording; +@property (nonatomic) NSString* currentViewName; +@property (nonatomic) NSString* previousViewName; + + (instancetype)sharedInstance; #if (TARGET_OS_IOS || TARGET_OS_TV) diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index f2366c94..de6410e7 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -17,6 +17,9 @@ @interface CountlyViewTrackingInternal () NSString* const kCountlyReservedEventView = @"[CLY]_view"; +NSString* const kCountlyCurrentView = @"cly_cvn"; +NSString* const kCountlyPreviousView = @"cly_pvn"; + NSString* const kCountlyVTKeyName = @"name"; NSString* const kCountlyVTKeySegment = @"segment"; NSString* const kCountlyVTKeyVisit = @"visit"; From 5009d89b8ad5a11e022e7b4e955b381ce639705a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 29 Aug 2024 19:24:14 +0500 Subject: [PATCH 045/148] reverted some changes related to pvid in non-view event and also created a method for segmentation --- Countly.m | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Countly.m b/Countly.m index 62c86830..1f0e7367 100644 --- a/Countly.m +++ b/Countly.m @@ -886,9 +886,11 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co event.ID = CountlyCommon.sharedInstance.randomEventID; } - event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; - - if(![key isEqualToString:kCountlyReservedEventView]) + if ([key isEqualToString:kCountlyReservedEventView]) + { + event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; + } + else { event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; } @@ -904,28 +906,33 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co previousEventID = event.ID; } event.key = key; + event.segmentation = [self processSegmentation:segmentation eventKey:key]; + event.count = MAX(count, 1); + event.sum = sum; + event.timestamp = timestamp; + event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; + event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; + event.duration = duration; + + [CountlyPersistency.sharedInstance recordEvent:event]; +} + +- (NSDictionary*) processSegmentation:(NSDictionary *) segmentation eventKey:(NSString *)eventKey +{ NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; if(CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording) { - if(CountlyViewTrackingInternal.sharedInstance.currentViewName && ![key isEqualToString:kCountlyReservedEventView]) { - filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName; - } - if(CountlyViewTrackingInternal.sharedInstance.previousViewName) { + if([eventKey isEqualToString:kCountlyReservedEventView]) { filteredSegmentations[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName; } + else { + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName; + } } if(CountlyCommon.sharedInstance.enableVisibiltyTracking) { filteredSegmentations[kCountlyVisibility] = @([self isAppInForeground]); } - event.segmentation = filteredSegmentations; - event.count = MAX(count, 1); - event.sum = sum; - event.timestamp = timestamp; - event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; - event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; - event.duration = duration; - - [CountlyPersistency.sharedInstance recordEvent:event]; + return filteredSegmentations; } - (BOOL)isAppInForeground { From a078a004a54f9f5a3c0e6cc432fb5a924f066e4a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 5 Sep 2024 15:32:00 +0500 Subject: [PATCH 046/148] Some fixes for view names --- Countly.m | 4 ++-- CountlyCommon.m | 2 +- CountlyConfig.h | 3 ++- CountlyViewTrackingInternal.m | 3 +++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Countly.m b/Countly.m index 1f0e7367..03381bf2 100644 --- a/Countly.m +++ b/Countly.m @@ -922,10 +922,10 @@ - (NSDictionary*) processSegmentation:(NSDictionary *) segmentation eventKey:(NS NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; if(CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording) { if([eventKey isEqualToString:kCountlyReservedEventView]) { - filteredSegmentations[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName; + filteredSegmentations[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; } else { - filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName; + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; } } diff --git a/CountlyCommon.m b/CountlyCommon.m index 27e61095..97926c66 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -660,7 +660,7 @@ - (NSMutableDictionary *) cly_filterSupportedDataTypes } } - return filteredDictionary.copy; + return filteredDictionary.mutableCopy; } @end diff --git a/CountlyConfig.h b/CountlyConfig.h index 633ceeae..92ba8ee5 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -652,13 +652,14 @@ typedef enum : NSUInteger @property (nonatomic) BOOL enableOrientationTracking; /** - * This is an experimental feature + * This is an experimental feature and it can have breaking changes * For enabling fetching and application of server config values. * @discussion If set, Server Config values from Countly Server will be fetched at the beginning of a session. */ @property (nonatomic) BOOL enableServerConfiguration; /** + * This is an experimental feature and it can have breaking changes * Variable to access experimental configurations. * @discussion Experimental configurations for developer to interact with SDK. */ diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index de6410e7..f6249898 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -443,6 +443,9 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio self.previousViewID = self.currentViewID; self.currentViewID = CountlyCommon.sharedInstance.randomEventID; + self.previousViewName = self.currentViewName; + self.currentViewName = viewName; + CountlyViewData *viewData = [[CountlyViewData alloc] initWithID:self.currentViewID viewName:viewName]; viewData.startSegmentation = customSegmentation.mutableCopy; viewData.isAutoStoppedView = isAutoStoppedView; From aee4c5586628a4f0d32510ce11e92e397ab4ceb4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 5 Sep 2024 16:44:15 +0500 Subject: [PATCH 047/148] Added experimental comments and updated method names --- Countly.h | 5 +++-- CountlyConfig.h | 4 +++- CountlyContentBuilder.h | 15 +++++++++++---- CountlyContentBuilder.m | 10 +++++----- CountlyContentBuilderInternal.h | 4 ++-- CountlyContentBuilderInternal.m | 8 ++++---- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Countly.h b/Countly.h index 94a33d09..7fe85635 100644 --- a/Countly.h +++ b/Countly.h @@ -680,8 +680,9 @@ NS_ASSUME_NONNULL_BEGIN /** - * Interface variable to access views tracking functionalities. - * @discussion Views tracking interface for developer to interact with SDK. + * This is an experimental feature and it can have breaking changes + * Interface variable to access content functionalities. + * @discussion Content interface for developer to interact with SDK. */ - (CountlyContentBuilder *_Nonnull) content; diff --git a/CountlyConfig.h b/CountlyConfig.h index 144210ab..59de9927 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -670,11 +670,13 @@ typedef enum : NSUInteger #if (TARGET_OS_IOS) /** - * Register global completion blocks to be executed content. + * This is an experimental feature and it can have breaking changes + * Register global completion blocks to be executed on content. */ - (void)setGlobalContentCallback:(ContentCallback) callback; /** + * This is an experimental feature and it can have breaking changes * Get content callback */ - (ContentCallback) getGlobalContentCallback; diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index ee18e76f..8a2901e8 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -12,10 +12,17 @@ + (instancetype)sharedInstance; -- (void)openForContent; -- (void)openForContent:(NSArray *)tags; -- (void)exitFromContent; -- (void)changeContent:(NSArray *)tags; +/** + * This is an experimental feature and it can have breaking changes + * Opt in user for the content fetching and updates + */ +- (void)subscribeToContentBlock; + +/** + * This is an experimental feature and it can have breaking changes + * Opt out user for the content fetching and updates + */ +- (void)exitFromContentBlock; @end #endif diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index b5ca6b8f..f0749a92 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -29,18 +29,18 @@ - (instancetype)init return self; } -- (void)openForContent +- (void)subscribeToContentBlock { [self openForContent:@[]]; } -- (void)openForContent:(NSArray *)tags +- (void)subscribeToContentBlock:(NSArray *)tags { - [CountlyContentBuilderInternal.sharedInstance openForContent:tags]; + [CountlyContentBuilderInternal.sharedInstance subscribeToContentBlock:tags]; } -- (void)exitFromContent +- (void)exitFromContentBlock { - [CountlyContentBuilderInternal.sharedInstance exitFromContent]; + [CountlyContentBuilderInternal.sharedInstance exitFromContentBlock]; } - (void)changeContent:(NSArray *)tags { diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 3620316d..94267e02 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -17,8 +17,8 @@ + (instancetype)sharedInstance; -- (void)openForContent:(NSArray *)tags; -- (void)exitFromContent; +- (void)subscribeToContentBlock:(NSArray *)tags; +- (void)exitFromContentBlock; - (void)changeContent:(NSArray *)tags; @end diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 66ab9ca9..9865b404 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -36,12 +36,12 @@ - (instancetype)init return self; } -- (void)openForContent:(NSArray *)tags { +- (void)subscribeToContentBlock:(NSArray *)tags { if (!CountlyConsentManager.sharedInstance.consentForContent) return; if(_requestTimer != nil) { - CLY_LOG_I(@"Already open for content, please exit from content first to start again"); + CLY_LOG_I(@"Already subscribed for content, please exit from content first to start again"); return; } self.currentTags = tags; @@ -54,14 +54,14 @@ - (void)openForContent:(NSArray *)tags { repeats:YES]; } -- (void)exitFromContent { +- (void)exitFromContentBlock { [self clearContentState]; } - (void)changeContent:(NSArray *)tags { if (![tags isEqualToArray:self.currentTags]) { [self exitFromContent]; - [self openForContent:tags]; + [self subscribeToContentBlock:tags]; } } From 3e524e69aa73d31992d9a8dc50db4b129dde4c67 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 5 Sep 2024 22:35:39 +0500 Subject: [PATCH 048/148] Again renamed content public method names --- CountlyContentBuilder.h | 4 ++-- CountlyContentBuilder.m | 8 ++++---- CountlyContentBuilderInternal.h | 4 ++-- CountlyContentBuilderInternal.m | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 8a2901e8..52d6ec57 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -16,13 +16,13 @@ * This is an experimental feature and it can have breaking changes * Opt in user for the content fetching and updates */ -- (void)subscribeToContentBlock; +- (void)enterContentZone; /** * This is an experimental feature and it can have breaking changes * Opt out user for the content fetching and updates */ -- (void)exitFromContentBlock; +- (void)exitContentZone; @end #endif diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index f0749a92..6ed253e9 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -34,13 +34,13 @@ - (void)subscribeToContentBlock [self openForContent:@[]]; } -- (void)subscribeToContentBlock:(NSArray *)tags +- (void)enterContentZone:(NSArray *)tags { - [CountlyContentBuilderInternal.sharedInstance subscribeToContentBlock:tags]; + [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; } -- (void)exitFromContentBlock +- (void)exitContentZone { - [CountlyContentBuilderInternal.sharedInstance exitFromContentBlock]; + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; } - (void)changeContent:(NSArray *)tags { diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 94267e02..a7c6398b 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -17,8 +17,8 @@ + (instancetype)sharedInstance; -- (void)subscribeToContentBlock:(NSArray *)tags; -- (void)exitFromContentBlock; +- (void)enterContentZone:(NSArray *)tags; +- (void)exitContentZone; - (void)changeContent:(NSArray *)tags; @end diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 9865b404..98e4f692 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -36,12 +36,12 @@ - (instancetype)init return self; } -- (void)subscribeToContentBlock:(NSArray *)tags { +- (void)enterContentZone:(NSArray *)tags { if (!CountlyConsentManager.sharedInstance.consentForContent) return; if(_requestTimer != nil) { - CLY_LOG_I(@"Already subscribed for content, please exit from content first to start again"); + CLY_LOG_I(@"Already entered for content zone, please exit from content zone first to start again"); return; } self.currentTags = tags; @@ -54,14 +54,14 @@ - (void)subscribeToContentBlock:(NSArray *)tags { repeats:YES]; } -- (void)exitFromContentBlock { +- (void)exitContentZone { [self clearContentState]; } - (void)changeContent:(NSArray *)tags { if (![tags isEqualToArray:self.currentTags]) { - [self exitFromContent]; - [self subscribeToContentBlock:tags]; + [self exitContentZone]; + [self enterContentZone:tags]; } } From fbe997a9ccf3e7622f4c1b41cd333d4b372fae67 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 5 Sep 2024 22:42:08 +0500 Subject: [PATCH 049/148] Updated SDK version to 24.7.2 and added changelog entry for content --- CHANGELOG.md | 3 ++- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f87a82..1875765f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -## xx.xx.xx +## 24.7.2 * Views will now be stopped and started again when the app transitions between background and foreground, instead of being paused and resumed. +* Introduced a new content feature aimed at boosting user engagement. (Experimental) ## 24.7.1 * Added `enableTemporaryDeviceIDMode` config and post-initialization methods to enable temporary device ID mode diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 9cf63e24..b0642548 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.1' + s.version = '24.7.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' diff --git a/Countly.podspec b/Countly.podspec index 9b5499ae..e53b86c8 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.1' + s.version = '24.7.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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d40d7984..1092a273 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -710,7 +710,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.1; + MARKETING_VERSION = 24.7.2; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -742,7 +742,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.1; + MARKETING_VERSION = 24.7.2; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index c673562c..83bace65 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -26,7 +26,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.1"; +NSString* const kCountlySDKVersion = @"24.7.2"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; From 9cee14d6d4ff475da3585689235fcefa5625f75f Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 5 Sep 2024 22:59:36 +0500 Subject: [PATCH 050/148] Stopping and resuming timer on the basis of content visibilty --- CountlyContentBuilderInternal.m | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 98e4f692..42018ccd 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -14,6 +14,7 @@ @implementation CountlyContentBuilderInternal { BOOL _isRequestQueueLocked; NSTimer *_requestTimer; + NSTimer *_minuteTimer; } + (instancetype)sharedInstance { @@ -36,7 +37,14 @@ - (instancetype)init return self; } +- (void)enterContentZone { + [self enterContentZone:@[]]; +} + - (void)enterContentZone:(NSArray *)tags { + [_minuteTimer invalidate]; + _minuteTimer = nil; + if (!CountlyConsentManager.sharedInstance.consentForContent) return; @@ -44,6 +52,7 @@ - (void)enterContentZone:(NSArray *)tags { CLY_LOG_I(@"Already entered for content zone, please exit from content zone first to start again"); return; } + self.currentTags = tags; [self fetchContents];; @@ -70,6 +79,9 @@ - (void)changeContent:(NSArray *)tags { - (void)clearContentState { [_requestTimer invalidate]; _requestTimer = nil; + + [_minuteTimer invalidate]; + _minuteTimer = nil; self.currentTags = nil; _isRequestQueueLocked = NO; } @@ -179,9 +191,15 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD [webViewManager createWebViewWithURL:url frame:frame appearBlock:^ { CLY_LOG_I(@"Webview appeared"); + [self clearContentState]; } dismissBlock:^ { CLY_LOG_I(@"Webview dismissed"); + self->_minuteTimer = [NSTimer scheduledTimerWithTimeInterval:60.0 + target:self + selector:@selector(enterContentZone) + userInfo:nil + repeats:NO]; if(self.contentCallback) { self.contentCallback(CLOSED, NSDictionary.new); } From 157564defe1eb761c88d05db1c87bfe8711f9736 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 11:55:21 +0500 Subject: [PATCH 051/148] Updated changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1875765f..86f40232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 24.7.2 * Views will now be stopped and started again when the app transitions between background and foreground, instead of being paused and resumed. -* Introduced a new content feature aimed at boosting user engagement. (Experimental) +* Added Content feature methods: + - enterContentZone, to start Content checks(Experimental!) + - exitContentZone, to stop content checks (Experimental!) ## 24.7.1 * Added `enableTemporaryDeviceIDMode` config and post-initialization methods to enable temporary device ID mode From fff8bd76d621715cd90c02017add5d30dc0474bd Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 12:19:58 +0500 Subject: [PATCH 052/148] Updated changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f40232..bee33845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 24.7.2 -* Views will now be stopped and started again when the app transitions between background and foreground, instead of being paused and resumed. -* Added Content feature methods: +* Automatic view pause/resumes are changed with stop/start for better data consistency. +* Added the config interface 'experimental' to group experimental features. +* Added a flag (enableViewNameRecording) to add view names to each event. (Experimental!) +* Added a flag (enableVisibilityTracking) to add app visibility info to views * Added Content feature methods: - enterContentZone, to start Content checks(Experimental!) - exitContentZone, to stop content checks (Experimental!) From 9f7a7cf4cdf8e8cd1f011150d9857a830dbafda7 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 14:55:36 +0500 Subject: [PATCH 053/148] Added a prev event name to the non reserved events --- Countly.m | 24 +++++++++++++----------- CountlyViewTrackingInternal.h | 3 +++ CountlyViewTrackingInternal.m | 3 +++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Countly.m b/Countly.m index ec8cd7fb..1c090c69 100644 --- a/Countly.m +++ b/Countly.m @@ -16,7 +16,8 @@ @interface Countly () long long appLoadStartTime; // It holds the event id of previous recorded custom event. NSString* previousEventID; - +// It holds the event name of previous recorded custom event. +NSString* previousEventName; @implementation Countly #pragma mark - Core @@ -903,15 +904,20 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co // Check if the event is a reserved event BOOL isReservedEvent = [self isReservedEvent:key]; - // If the event is not reserved, assign the previous event ID to the current event's PEID property, or an empty string if previousEventID is nil. Then, update previousEventID to the current event's ID. + NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; + + // If the event is not reserved, assign the previous event ID and Name to the current event's PEID property, or an empty string if previousEventID is nil. Then, update previousEventID to the current event's ID. if (!isReservedEvent) { key = [key cly_truncatedKey:@"Event key"]; event.PEID = previousEventID ?: @""; previousEventID = event.ID; + + filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; + previousEventName = key; } event.key = key; - event.segmentation = [self processSegmentation:segmentation eventKey:key]; + event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; event.count = MAX(count, 1); event.sum = sum; event.timestamp = timestamp; @@ -922,22 +928,18 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co [CountlyPersistency.sharedInstance recordEvent:event]; } -- (NSDictionary*) processSegmentation:(NSDictionary *) segmentation eventKey:(NSString *)eventKey +- (NSDictionary*) processSegmentation:(NSMutableDictionary *) segmentation eventKey:(NSString *)eventKey { - NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; if(CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording) { if([eventKey isEqualToString:kCountlyReservedEventView]) { - filteredSegmentations[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; - } - else { - filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; + segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; } } if(CountlyCommon.sharedInstance.enableVisibiltyTracking) { - filteredSegmentations[kCountlyVisibility] = @([self isAppInForeground]); + segmentation[kCountlyVisibility] = @([self isAppInForeground]); } - return filteredSegmentations; + return segmentation; } - (BOOL)isAppInForeground { diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index 3c089493..4920ffbe 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -8,8 +8,10 @@ extern NSString* const kCountlyReservedEventView; +//TODO: Need discussion for its usage and then we decide to keep it or remove it extern NSString* const kCountlyCurrentView; extern NSString* const kCountlyPreviousView; +extern NSString* const kCountlyPreviousEventName; @interface CountlyViewTrackingInternal : NSObject @property (nonatomic) BOOL isEnabledOnInitialConfig; @@ -17,6 +19,7 @@ extern NSString* const kCountlyPreviousView; @property (nonatomic) NSString* previousViewID; @property (nonatomic) BOOL enableViewNameRecording; +//TODO: Need discussion for its usage and then we decide to keep it or remove it @property (nonatomic) NSString* currentViewName; @property (nonatomic) NSString* previousViewName; diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index f6249898..defeab6a 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -17,9 +17,12 @@ @interface CountlyViewTrackingInternal () NSString* const kCountlyReservedEventView = @"[CLY]_view"; +//TODO: Need discussion for its usage and then we decide to keep it or remove it NSString* const kCountlyCurrentView = @"cly_cvn"; NSString* const kCountlyPreviousView = @"cly_pvn"; +NSString* const kCountlyPreviousEventName = @"cly_pen"; + NSString* const kCountlyVTKeyName = @"name"; NSString* const kCountlyVTKeySegment = @"segment"; NSString* const kCountlyVTKeyVisit = @"visit"; From 700353715d3d12572185ff58aded61d4ff10e31d Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 16:44:55 +0500 Subject: [PATCH 054/148] Updated changelog for previous event name --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee33845..75b26dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 24.7.2 * Automatic view pause/resumes are changed with stop/start for better data consistency. * Added the config interface 'experimental' to group experimental features. +* Added support for including the previous event name when recording custom events. * Added a flag (enableViewNameRecording) to add view names to each event. (Experimental!) * Added a flag (enableVisibilityTracking) to add app visibility info to views * Added Content feature methods: - enterContentZone, to start Content checks(Experimental!) From 884d831e711ca7ca2a8a658ecc987ee21bbfe705 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 17:03:04 +0500 Subject: [PATCH 055/148] Updated changelog, flag name and podspec files --- CHANGELOG.md | 5 +++-- Countly-PL.podspec | 2 +- Countly.m | 6 +++--- Countly.podspec | 2 +- CountlyExperimentalConfig.h | 2 +- CountlyExperimentalConfig.m | 2 +- CountlyViewTrackingInternal.h | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b26dff..ec804bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ * Automatic view pause/resumes are changed with stop/start for better data consistency. * Added the config interface 'experimental' to group experimental features. * Added support for including the previous event name when recording custom events. -* Added a flag (enableViewNameRecording) to add view names to each event. (Experimental!) -* Added a flag (enableVisibilityTracking) to add app visibility info to views * Added Content feature methods: +* Added a flag (enablePreviousNameRecording) to add previous event and view names as segmentation. (Experimental!) +* Added a flag (enableVisibilityTracking) to add app visibility info to views +* Added Content feature methods: - enterContentZone, to start Content checks(Experimental!) - exitContentZone, to stop content checks (Experimental!) diff --git a/Countly-PL.podspec b/Countly-PL.podspec index b0642548..45fa4327 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/Countly.m b/Countly.m index 1c090c69..5c8fd459 100644 --- a/Countly.m +++ b/Countly.m @@ -246,8 +246,8 @@ - (void)startWithConfig:(CountlyConfig *)config } #endif - if(config.experimental.enableViewNameRecording) { - CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording = YES; + if(config.experimental.enablePreviousNameRecording) { + CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording = YES; } if(config.experimental.enableVisibiltyTracking) { CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; @@ -930,7 +930,7 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co - (NSDictionary*) processSegmentation:(NSMutableDictionary *) segmentation eventKey:(NSString *)eventKey { - if(CountlyViewTrackingInternal.sharedInstance.enableViewNameRecording) { + if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { if([eventKey isEqualToString:kCountlyReservedEventView]) { segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; } diff --git a/Countly.podspec b/Countly.podspec index e53b86c8..4e0a25e7 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/CountlyExperimentalConfig.h b/CountlyExperimentalConfig.h index 89644bcd..7e438310 100644 --- a/CountlyExperimentalConfig.h +++ b/CountlyExperimentalConfig.h @@ -10,7 +10,7 @@ extern NSString* const kCountlySCKeySC; @interface CountlyExperimentalConfig : NSObject -@property (nonatomic) BOOL enableViewNameRecording; +@property (nonatomic) BOOL enablePreviousNameRecording; @property (nonatomic) BOOL enableVisibiltyTracking; @end diff --git a/CountlyExperimentalConfig.m b/CountlyExperimentalConfig.m index 41432ea0..e01f3f23 100644 --- a/CountlyExperimentalConfig.m +++ b/CountlyExperimentalConfig.m @@ -13,7 +13,7 @@ - (instancetype)init if (self = [super init]) { self.enableVisibiltyTracking = NO; - self.enableViewNameRecording = NO; + self.enablePreviousNameRecording = NO; } return self; diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index 4920ffbe..cc5ff653 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -18,7 +18,7 @@ extern NSString* const kCountlyPreviousEventName; @property (nonatomic) NSString* currentViewID; @property (nonatomic) NSString* previousViewID; -@property (nonatomic) BOOL enableViewNameRecording; +@property (nonatomic) BOOL enablePreviousNameRecording; //TODO: Need discussion for its usage and then we decide to keep it or remove it @property (nonatomic) NSString* currentViewName; @property (nonatomic) NSString* previousViewName; From e121a0cf6d5caaae21721e9330fe761babf3ca86 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:05:03 +0900 Subject: [PATCH 056/148] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec804bac..eafa1d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ ## 24.7.2 * Automatic view pause/resumes are changed with stop/start for better data consistency. * Added the config interface 'experimental' to group experimental features. -* Added support for including the previous event name when recording custom events. * Added a flag (enablePreviousNameRecording) to add previous event and view names as segmentation. (Experimental!) * Added a flag (enableVisibilityTracking) to add app visibility info to views * Added Content feature methods: From f466e974ff5ff9595955e2576fc2f0c2b8a949ac Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 17:49:54 +0500 Subject: [PATCH 057/148] Added a separate content config --- Countly.m | 4 ++-- Countly.xcodeproj/project.pbxproj | 8 +++++++ CountlyConfig.h | 24 ++++--------------- CountlyConfig.m | 23 +++++++----------- CountlyContentConfig.h | 39 +++++++++++++++++++++++++++++++ CountlyContentConfig.m | 38 ++++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 37 deletions(-) create mode 100644 CountlyContentConfig.h create mode 100644 CountlyContentConfig.m diff --git a/Countly.m b/Countly.m index 5c8fd459..ff61c72a 100644 --- a/Countly.m +++ b/Countly.m @@ -272,8 +272,8 @@ - (void)startWithConfig:(CountlyConfig *)config appLoadStartTime = config.apm.getAppStartTimestampOverride; } #if (TARGET_OS_IOS) - if(config.getGlobalContentCallback) { - CountlyContentBuilderInternal.sharedInstance.contentCallback = config.getGlobalContentCallback; + if(config.content.getGlobalContentCallback) { + CountlyContentBuilderInternal.sharedInstance.contentCallback = config.content.getGlobalContentCallback; } #endif diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index c7667476..de11d13b 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 1ACA5DC12A309E7F001F770B /* CountlyRemoteConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACA5DBF2A309E7F001F770B /* CountlyRemoteConfigInternal.h */; }; 1ACA5DC22A309E7F001F770B /* CountlyRemoteConfigInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACA5DC02A309E7F001F770B /* CountlyRemoteConfigInternal.m */; }; 1AFD79022B3EF82C00772FBD /* CountlyTests-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */; }; + 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39002D092C8B2E450049394F /* CountlyContentConfig.h */; }; + 39002D0C2C8B2E450049394F /* CountlyContentConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */; }; 3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */; }; 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3948A8552BAC2E7D002D09AA /* CountlySDKLimitsConfig.m */; }; 3948A8582BAC2E7D002D09AA /* CountlySDKLimitsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3948A8562BAC2E7D002D09AA /* CountlySDKLimitsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -113,6 +115,8 @@ 1ACA5DBF2A309E7F001F770B /* CountlyRemoteConfigInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyRemoteConfigInternal.h; sourceTree = ""; }; 1ACA5DC02A309E7F001F770B /* CountlyRemoteConfigInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyRemoteConfigInternal.m; sourceTree = ""; }; 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CountlyTests-Bridging-Header.h"; sourceTree = ""; }; + 39002D092C8B2E450049394F /* CountlyContentConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyContentConfig.h; sourceTree = ""; }; + 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentConfig.m; sourceTree = ""; }; 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyExperimentalConfig.h; sourceTree = ""; }; 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyExperimentalConfig.m; sourceTree = ""; }; 3948A8552BAC2E7D002D09AA /* CountlySDKLimitsConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlySDKLimitsConfig.m; sourceTree = ""; }; @@ -219,6 +223,8 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( + 39002D092C8B2E450049394F /* CountlyContentConfig.h */, + 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */, 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */, 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */, 3961C6B12C6633C000DD38BA /* PassThroughBackgroundView.h */, @@ -350,6 +356,7 @@ 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */, 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */, D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */, + 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -487,6 +494,7 @@ 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, + 39002D0C2C8B2E450049394F /* CountlyContentConfig.m in Sources */, 3B20A9C02245228700E3D7AE /* CountlyConsentManager.m in Sources */, 39527E152B5FD27400EE5D7B /* CountlyAPMConfig.m in Sources */, 1A3110712A7141AF001CB507 /* CountlyViewTracking.m in Sources */, diff --git a/CountlyConfig.h b/CountlyConfig.h index c9732d5b..a3b6246d 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -11,6 +11,7 @@ #import "CountlyCrashesConfig.h" #import "CountlySDKLimitsConfig.h" #import "CountlyExperimentalConfig.h" +#import "CountlyContentConfig.h" #if (TARGET_OS_IOS || TARGET_OS_TV) #import @@ -116,15 +117,6 @@ typedef void (^RCVariantCallback)(CLYRequestResult response, NSError *_Nullable typedef void (^RCDownloadCallback)(CLYRequestResult response, NSError *_Nullable error, BOOL fullValueUpdate, NSDictionary* downloadedValues); -#if (TARGET_OS_IOS) -typedef enum : NSUInteger -{ - COMPLETED, - CLOSED, -} ContentStatus; - -typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary* contentData); -#endif //NOTE: Internal log levels typedef enum : NSUInteger @@ -669,19 +661,11 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL enableServerConfiguration; -#if (TARGET_OS_IOS) - /** - * This is an experimental feature and it can have breaking changes - * Register global completion blocks to be executed on content. - */ -- (void)setGlobalContentCallback:(ContentCallback) callback; - /** - * This is an experimental feature and it can have breaking changes - * Get content callback + * Variable to access content configurations. + * @discussion Content configurations for developer to interact with SDK. */ -- (ContentCallback) getGlobalContentCallback; -#endif +- (CountlyContentConfig *) content; /** * This is an experimental feature and it can have breaking changes diff --git a/CountlyConfig.m b/CountlyConfig.m index 27947997..47a38968 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -8,9 +8,6 @@ @interface CountlyConfig () @property (nonatomic) NSMutableArray *remoteConfigGlobalCallbacks; -#if (TARGET_OS_IOS) -@property (nonatomic) ContentCallback contentCallback; -#endif @end @interface CountlyAPMConfig () @@ -40,6 +37,7 @@ @implementation CountlyConfig CountlyCrashesConfig *crashes = nil; CountlySDKLimitsConfig *sdkLimitsConfig = nil; CountlyExperimentalConfig *experimental = nil; +CountlyContentConfig *content = nil; //NOTE: Device ID options NSString* const CLYDefaultDeviceID = @""; //NOTE: It will be overridden to default device ID mechanism, depending on platform. @@ -128,18 +126,6 @@ - (nonnull CountlyCrashesConfig *)crashes { return crashes; } -#if (TARGET_OS_IOS) --(void)setGlobalContentCallback:(ContentCallback) callback -{ - self.contentCallback = callback; -} - -- (ContentCallback) getGlobalContentCallback -{ - return self.contentCallback; -} -#endif - - (nonnull CountlyExperimentalConfig *)experimental { if (experimental == nil) { experimental = CountlyExperimentalConfig.new; @@ -147,4 +133,11 @@ - (nonnull CountlyExperimentalConfig *)experimental { return experimental; } +- (nonnull CountlyContentConfig *)content { + if (content == nil) { + content = CountlyContentConfig.new; + } + return content; +} + @end diff --git a/CountlyContentConfig.h b/CountlyContentConfig.h new file mode 100644 index 00000000..d72fac0c --- /dev/null +++ b/CountlyContentConfig.h @@ -0,0 +1,39 @@ +// CountlyContentConfig.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if (TARGET_OS_IOS) +typedef enum : NSUInteger +{ + COMPLETED, + CLOSED, +} ContentStatus; + +typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary* contentData); +#endif + +@interface CountlyContentConfig : NSObject + +#if (TARGET_OS_IOS) +/** + * This is an experimental feature and it can have breaking changes + * Register global completion blocks to be executed on content. + */ +- (void)setGlobalContentCallback:(ContentCallback) callback; + +/** + * This is an experimental feature and it can have breaking changes + * Get content callback + */ +- (ContentCallback) getGlobalContentCallback; +#endif + +NS_ASSUME_NONNULL_END + +@end diff --git a/CountlyContentConfig.m b/CountlyContentConfig.m new file mode 100644 index 00000000..3d7d63cd --- /dev/null +++ b/CountlyContentConfig.m @@ -0,0 +1,38 @@ +// CountlyContentConfig.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyCommon.h" + +@interface CountlyContentConfig () +#if (TARGET_OS_IOS) +@property (nonatomic) ContentCallback contentCallback; +#endif +@end + +@implementation CountlyContentConfig + +- (instancetype)init +{ + if (self = [super init]) + { + } + + return self; +} + +#if (TARGET_OS_IOS) +-(void)setGlobalContentCallback:(ContentCallback) callback +{ + self.contentCallback = callback; +} + +- (ContentCallback) getGlobalContentCallback +{ + return self.contentCallback; +} +#endif + +@end From 882f3c8b340edb98e7b2d3cbc23f684dbed5448e Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 18:34:31 +0500 Subject: [PATCH 058/148] Content fixes --- Countly.xcodeproj/project.pbxproj | 4 +++- CountlyConsentManager.m | 2 +- CountlyContentBuilder.h | 9 +++++---- CountlyContentBuilder.m | 9 ++++----- CountlyContentBuilderInternal.h | 8 ++++---- CountlyContentBuilderInternal.m | 5 ++--- CountlyWebViewManager.h | 12 +++++++++--- CountlyWebViewManager.m | 7 ++++--- PassThroughBackgroundView.h | 9 +++++---- PassThroughBackgroundView.m | 4 ++-- 10 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index c7667476..8104fffb 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -39,13 +39,14 @@ 399117D12C69F73D00DC4C66 /* CountlyContentBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */; }; 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 399117CE2C69F73D00DC4C66 /* CountlyContentBuilderInternal.h */; }; 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CF2C69F73D00DC4C66 /* CountlyContentBuilderInternal.m */; }; - 399117D42C69F73D00DC4C66 /* CountlyContentBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 399117D02C69F73D00DC4C66 /* CountlyContentBuilder.h */; }; + 399117D42C69F73D00DC4C66 /* CountlyContentBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 399117D02C69F73D00DC4C66 /* CountlyContentBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39911B672B457DBB00AC053C /* Resettable.h in Headers */ = {isa = PBXBuildFile; fileRef = 39911B662B457DB500AC053C /* Resettable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39924ECE2BEBD0B700139F91 /* CountlyCrashesConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ECD2BEBD0B700139F91 /* CountlyCrashesConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39924ED02BEBD0D400139F91 /* CountlyCrashesConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ECF2BEBD0D400139F91 /* CountlyCrashesConfig.m */; }; 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */; }; 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ED72BEBD22100139F91 /* CountlyCrashData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399B464F2C52813700AD384E /* CountlyLocationTests.swift */; }; + 39EE1F102C8B341E0016D1BF /* CountlyExperimentalConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9872245225A00E3D7AE /* Countly.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9852245225A00E3D7AE /* Countly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A98D2245228300E3D7AE /* CountlyConnectionManager.h */; }; 3B20A9B32245228700E3D7AE /* CountlyNotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B20A98E2245228300E3D7AE /* CountlyNotificationService.m */; }; @@ -332,6 +333,7 @@ 3961C6B52C6633C000DD38BA /* CountlyWebViewManager.h in Headers */, 1ACA5DC12A309E7F001F770B /* CountlyRemoteConfigInternal.h in Headers */, 3B20A9CC2245228700E3D7AE /* CountlyViewTrackingInternal.h in Headers */, + 39EE1F102C8B341E0016D1BF /* CountlyExperimentalConfig.h in Headers */, 3948A8582BAC2E7D002D09AA /* CountlySDKLimitsConfig.h in Headers */, 3B20A9BF2245228700E3D7AE /* CountlyLocationManager.h in Headers */, D219374B248AC71C00E5798B /* CountlyPerformanceMonitoring.h in Headers */, diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 9f01acf8..8fa89c9e 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -474,7 +474,7 @@ - (void)setConsentForContent:(BOOL)consentForContent { CLY_LOG_D(@"Consent for Content is cancelled."); #if (TARGET_OS_IOS) - [CountlyContentBuilderInternal.sharedInstance exitFromContent]; + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; #endif } } diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 52d6ec57..3614413a 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -4,12 +4,12 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import #import - +NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilder: NSObject - +#if (TARGET_OS_IOS) + (instancetype)sharedInstance; /** @@ -24,5 +24,6 @@ */ - (void)exitContentZone; -@end #endif +NS_ASSUME_NONNULL_END +@end diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index 6ed253e9..6a456cea 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -4,13 +4,12 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) #import "CountlyContentBuilder.h" #import "CountlyContentBuilderInternal.h" #import "CountlyCommon.h" @implementation CountlyContentBuilder - +#if (TARGET_OS_IOS) + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) @@ -29,9 +28,9 @@ - (instancetype)init return self; } -- (void)subscribeToContentBlock +- (void)enterContentZone { - [self openForContent:@[]]; + [self enterContentZone:@[]]; } - (void)enterContentZone:(NSArray *)tags @@ -47,5 +46,5 @@ - (void)changeContent:(NSArray *)tags [CountlyContentBuilder.sharedInstance changeContent:tags]; } -@end #endif +@end diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index a7c6398b..905fb0b1 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -4,13 +4,12 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) #import #import #import "CountlyCommon.h" - +NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilderInternal: NSObject - +#if (TARGET_OS_IOS) @property (nonatomic, strong) NSArray *currentTags; @property (nonatomic, assign) NSTimeInterval requestInterval; @property (nonatomic) ContentCallback contentCallback; @@ -21,6 +20,7 @@ - (void)exitContentZone; - (void)changeContent:(NSArray *)tags; -@end #endif +NS_ASSUME_NONNULL_END +@end diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 42018ccd..4a0ef01b 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -3,7 +3,6 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) #import "CountlyContentBuilderInternal.h" #import "CountlyWebViewManager.h" @@ -16,7 +15,7 @@ @implementation CountlyContentBuilderInternal { NSTimer *_requestTimer; NSTimer *_minuteTimer; } - +#if (TARGET_OS_IOS) + (instancetype)sharedInstance { static CountlyContentBuilderInternal *instance = nil; static dispatch_once_t onceToken; @@ -206,5 +205,5 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD }]; }); } -@end #endif +@end diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index c2d09617..77748adb 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -4,10 +4,12 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import #import +NS_ASSUME_NONNULL_BEGIN +#if (TARGET_OS_IOS) typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeSlideInFromBottom, AnimationTypeSlideInFromTop, @@ -17,13 +19,17 @@ typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeIncreaseHeightFromBottom }; -@interface CountlyWebViewManager : NSObject +#endif +@interface CountlyWebViewManager : NSObject +#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock dismissBlock:(void(^ __nullable)(void))dismissBlock; -@end #endif + +NS_ASSUME_NONNULL_END +@end diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index 1ae3d71f..4b356df9 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -1,17 +1,18 @@ -#if (TARGET_OS_IOS) #import "CountlyWebViewManager.h" #import "PassThroughBackgroundView.h" #import "CountlyCommon.h" //TODO: improve logging, check edge cases @interface CountlyWebViewManager() +#if (TARGET_OS_IOS) @property (nonatomic, strong) PassThroughBackgroundView *backgroundView; @property (nonatomic, copy) void (^dismissBlock)(void); +#endif @end @implementation CountlyWebViewManager - +#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock @@ -243,5 +244,5 @@ - (void)closeWebView { [self.backgroundView removeFromSuperview]; }); } -@end #endif +@end diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h index 722cb1ba..78e79ffa 100644 --- a/PassThroughBackgroundView.h +++ b/PassThroughBackgroundView.h @@ -4,16 +4,17 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import #import #import "CountlyCommon.h" - +NS_ASSUME_NONNULL_BEGIN @interface PassThroughBackgroundView : UIView +#if (TARGET_OS_IOS) @property (nonatomic, strong) WKWebView *webView; @property (nonatomic, strong) CLYButton *dismissButton; +#endif - +NS_ASSUME_NONNULL_END @end -#endif diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m index b9af0370..cd8bf876 100644 --- a/PassThroughBackgroundView.m +++ b/PassThroughBackgroundView.m @@ -4,7 +4,6 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) #import "PassThroughBackgroundView.h" @implementation PassThroughBackgroundView @@ -12,6 +11,7 @@ @implementation PassThroughBackgroundView @synthesize webView; - (instancetype)initWithFrame:(CGRect)frame { +#if (TARGET_OS_IOS) self = [super initWithFrame:frame]; if (self) { } @@ -30,5 +30,5 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return NO; } -@end #endif +@end From 4546d90ab73bafa6b697939f3b455d4286eaf1c6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 18:46:35 +0500 Subject: [PATCH 059/148] "CountlyContentConfig.h" public file --- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 45fa4327..4a9d537c 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/Countly.podspec b/Countly.podspec index 4e0a25e7..7650edfc 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index bcad0e20..d29acd28 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -23,7 +23,7 @@ 1ACA5DC12A309E7F001F770B /* CountlyRemoteConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACA5DBF2A309E7F001F770B /* CountlyRemoteConfigInternal.h */; }; 1ACA5DC22A309E7F001F770B /* CountlyRemoteConfigInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACA5DC02A309E7F001F770B /* CountlyRemoteConfigInternal.m */; }; 1AFD79022B3EF82C00772FBD /* CountlyTests-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */; }; - 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39002D092C8B2E450049394F /* CountlyContentConfig.h */; }; + 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 39002D092C8B2E450049394F /* CountlyContentConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39002D0C2C8B2E450049394F /* CountlyContentConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */; }; 3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3903429C2C8051C700238C96 /* CountlyExperimentalConfig.m */; }; 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3948A8552BAC2E7D002D09AA /* CountlySDKLimitsConfig.m */; }; From 254a7c3479fd9cd4c7b381d2ce9bedbba3bbed8c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 20:36:29 +0500 Subject: [PATCH 060/148] Some more fixes --- Countly.m | 7 ++++--- CountlyContentBuilder.h | 8 +++++--- CountlyContentBuilder.m | 9 ++++++--- CountlyContentBuilderInternal.h | 9 +++++---- CountlyContentBuilderInternal.m | 8 ++++++-- CountlyWebViewManager.h | 12 ++++++------ CountlyWebViewManager.m | 11 ++++++----- PassThroughBackgroundView.h | 6 ++---- PassThroughBackgroundView.m | 6 ++---- 9 files changed, 42 insertions(+), 34 deletions(-) diff --git a/Countly.m b/Countly.m index ff61c72a..83dff794 100644 --- a/Countly.m +++ b/Countly.m @@ -912,9 +912,10 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co key = [key cly_truncatedKey:@"Event key"]; event.PEID = previousEventID ?: @""; previousEventID = event.ID; - - filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; - previousEventName = key; + if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { + filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; + previousEventName = key; + } } event.key = key; event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 3614413a..088d58be 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -6,10 +6,9 @@ #import -#import +#if (TARGET_OS_IOS) NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilder: NSObject -#if (TARGET_OS_IOS) + (instancetype)sharedInstance; /** @@ -24,6 +23,9 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)exitContentZone; -#endif NS_ASSUME_NONNULL_END @end +#else +@interface CountlyContentBuilder: NSObject +@end +#endif diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index 6a456cea..9e2f45e4 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -3,13 +3,12 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. - +#if (TARGET_OS_IOS) #import "CountlyContentBuilder.h" #import "CountlyContentBuilderInternal.h" #import "CountlyCommon.h" @implementation CountlyContentBuilder -#if (TARGET_OS_IOS) + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) @@ -46,5 +45,9 @@ - (void)changeContent:(NSArray *)tags [CountlyContentBuilder.sharedInstance changeContent:tags]; } -#endif @end +#else +#import "CountlyContentBuilder.h" +@implementation CountlyContentBuilder +@end +#endif diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 905fb0b1..cef5ce2a 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -5,11 +5,10 @@ // Please visit www.count.ly for more information. #import -#import #import "CountlyCommon.h" +#if (TARGET_OS_IOS) NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilderInternal: NSObject -#if (TARGET_OS_IOS) @property (nonatomic, strong) NSArray *currentTags; @property (nonatomic, assign) NSTimeInterval requestInterval; @property (nonatomic) ContentCallback contentCallback; @@ -20,7 +19,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)exitContentZone; - (void)changeContent:(NSArray *)tags; -#endif NS_ASSUME_NONNULL_END @end - +#else +@interface CountlyContentBuilderInternal: NSObject +@end +#endif diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 4a0ef01b..3b43554e 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -3,7 +3,9 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. + #import "CountlyContentBuilderInternal.h" +#if (TARGET_OS_IOS) #import "CountlyWebViewManager.h" //TODO: improve logging, check edge cases @@ -15,7 +17,6 @@ @implementation CountlyContentBuilderInternal { NSTimer *_requestTimer; NSTimer *_minuteTimer; } -#if (TARGET_OS_IOS) + (instancetype)sharedInstance { static CountlyContentBuilderInternal *instance = nil; static dispatch_once_t onceToken; @@ -205,5 +206,8 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD }]; }); } -#endif @end +#else +@implementation CountlyContentBuilderInternal +@end +#endif diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index 77748adb..2b496eb8 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -4,12 +4,11 @@ // // Please visit www.count.ly for more information. - +#if (TARGET_OS_IOS) #import #import NS_ASSUME_NONNULL_BEGIN -#if (TARGET_OS_IOS) typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeSlideInFromBottom, AnimationTypeSlideInFromTop, @@ -19,17 +18,18 @@ typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeIncreaseHeightFromBottom }; -#endif - @interface CountlyWebViewManager : NSObject -#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock dismissBlock:(void(^ __nullable)(void))dismissBlock; -#endif NS_ASSUME_NONNULL_END @end +#else +#import +@interface CountlyWebViewManager : NSObject +@end +#endif diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index 4b356df9..d20d53d0 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -1,18 +1,15 @@ - +#if (TARGET_OS_IOS) #import "CountlyWebViewManager.h" #import "PassThroughBackgroundView.h" #import "CountlyCommon.h" //TODO: improve logging, check edge cases @interface CountlyWebViewManager() -#if (TARGET_OS_IOS) @property (nonatomic, strong) PassThroughBackgroundView *backgroundView; @property (nonatomic, copy) void (^dismissBlock)(void); -#endif @end @implementation CountlyWebViewManager -#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock @@ -244,5 +241,9 @@ - (void)closeWebView { [self.backgroundView removeFromSuperview]; }); } -#endif @end +#else +#import "CountlyWebViewManager.h" +@implementation CountlyWebViewManager +@end +#endif diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h index 78e79ffa..04df7fbc 100644 --- a/PassThroughBackgroundView.h +++ b/PassThroughBackgroundView.h @@ -4,17 +4,15 @@ // // Please visit www.count.ly for more information. - +#if (TARGET_OS_IOS) #import #import #import "CountlyCommon.h" NS_ASSUME_NONNULL_BEGIN @interface PassThroughBackgroundView : UIView - -#if (TARGET_OS_IOS) @property (nonatomic, strong) WKWebView *webView; @property (nonatomic, strong) CLYButton *dismissButton; -#endif NS_ASSUME_NONNULL_END @end +#endif diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m index cd8bf876..d7c43e09 100644 --- a/PassThroughBackgroundView.m +++ b/PassThroughBackgroundView.m @@ -3,7 +3,7 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. - +#if (TARGET_OS_IOS) #import "PassThroughBackgroundView.h" @implementation PassThroughBackgroundView @@ -11,7 +11,6 @@ @implementation PassThroughBackgroundView @synthesize webView; - (instancetype)initWithFrame:(CGRect)frame { -#if (TARGET_OS_IOS) self = [super initWithFrame:frame]; if (self) { } @@ -29,6 +28,5 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return NO; } - -#endif @end +#endif From a05fc1414badc52728533267762576b882aa0f9d Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 20:56:56 +0500 Subject: [PATCH 061/148] Added public files in include folder --- include/CountlyContentBuilder.h | 1 + include/CountlyContentConfig.h | 1 + include/CountlyExperimentalConfig.h | 1 + 3 files changed, 3 insertions(+) create mode 120000 include/CountlyContentBuilder.h create mode 120000 include/CountlyContentConfig.h create mode 120000 include/CountlyExperimentalConfig.h diff --git a/include/CountlyContentBuilder.h b/include/CountlyContentBuilder.h new file mode 120000 index 00000000..eac7b004 --- /dev/null +++ b/include/CountlyContentBuilder.h @@ -0,0 +1 @@ +../CountlyContentBuilder.h \ No newline at end of file diff --git a/include/CountlyContentConfig.h b/include/CountlyContentConfig.h new file mode 120000 index 00000000..6a8fafd0 --- /dev/null +++ b/include/CountlyContentConfig.h @@ -0,0 +1 @@ +../CountlyContentConfig.h \ No newline at end of file diff --git a/include/CountlyExperimentalConfig.h b/include/CountlyExperimentalConfig.h new file mode 120000 index 00000000..77ddc337 --- /dev/null +++ b/include/CountlyExperimentalConfig.h @@ -0,0 +1 @@ +../CountlyExperimentalConfig.h \ No newline at end of file From a531e20314e0dda7111b60344c17776b25a3d3d4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 21:04:17 +0500 Subject: [PATCH 062/148] Giving all consents for content consents --- CountlyConsentManager.m | 1 + 1 file changed, 1 insertion(+) diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 8fa89c9e..fc4bf12f 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -228,6 +228,7 @@ - (NSArray *)allFeatures CLYConsentPerformanceMonitoring, CLYConsentFeedback, CLYConsentRemoteConfig, + CLYConsentContent ]; } From 9ae3f90abde335b1cb7a0567e2d02133108db9ba Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 6 Sep 2024 21:12:58 +0500 Subject: [PATCH 063/148] Revert "Some more fixes" This reverts commit 254a7c3479fd9cd4c7b381d2ce9bedbba3bbed8c. --- Countly.m | 7 +++---- CountlyContentBuilder.h | 8 +++----- CountlyContentBuilder.m | 9 +++------ CountlyContentBuilderInternal.h | 9 ++++----- CountlyContentBuilderInternal.m | 8 ++------ CountlyWebViewManager.h | 12 ++++++------ CountlyWebViewManager.m | 11 +++++------ PassThroughBackgroundView.h | 6 ++++-- PassThroughBackgroundView.m | 6 ++++-- 9 files changed, 34 insertions(+), 42 deletions(-) diff --git a/Countly.m b/Countly.m index 83dff794..ff61c72a 100644 --- a/Countly.m +++ b/Countly.m @@ -912,10 +912,9 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co key = [key cly_truncatedKey:@"Event key"]; event.PEID = previousEventID ?: @""; previousEventID = event.ID; - if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; - previousEventName = key; - } + + filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; + previousEventName = key; } event.key = key; event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 088d58be..3614413a 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -6,9 +6,10 @@ #import -#if (TARGET_OS_IOS) +#import NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilder: NSObject +#if (TARGET_OS_IOS) + (instancetype)sharedInstance; /** @@ -23,9 +24,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)exitContentZone; +#endif NS_ASSUME_NONNULL_END @end -#else -@interface CountlyContentBuilder: NSObject -@end -#endif diff --git a/CountlyContentBuilder.m b/CountlyContentBuilder.m index 9e2f45e4..6a456cea 100644 --- a/CountlyContentBuilder.m +++ b/CountlyContentBuilder.m @@ -3,12 +3,13 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import "CountlyContentBuilder.h" #import "CountlyContentBuilderInternal.h" #import "CountlyCommon.h" @implementation CountlyContentBuilder +#if (TARGET_OS_IOS) + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) @@ -45,9 +46,5 @@ - (void)changeContent:(NSArray *)tags [CountlyContentBuilder.sharedInstance changeContent:tags]; } -@end -#else -#import "CountlyContentBuilder.h" -@implementation CountlyContentBuilder -@end #endif +@end diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index cef5ce2a..905fb0b1 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -5,10 +5,11 @@ // Please visit www.count.ly for more information. #import +#import #import "CountlyCommon.h" -#if (TARGET_OS_IOS) NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilderInternal: NSObject +#if (TARGET_OS_IOS) @property (nonatomic, strong) NSArray *currentTags; @property (nonatomic, assign) NSTimeInterval requestInterval; @property (nonatomic) ContentCallback contentCallback; @@ -19,9 +20,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)exitContentZone; - (void)changeContent:(NSArray *)tags; +#endif NS_ASSUME_NONNULL_END @end -#else -@interface CountlyContentBuilderInternal: NSObject -@end -#endif + diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 3b43554e..4a0ef01b 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -3,9 +3,7 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. - #import "CountlyContentBuilderInternal.h" -#if (TARGET_OS_IOS) #import "CountlyWebViewManager.h" //TODO: improve logging, check edge cases @@ -17,6 +15,7 @@ @implementation CountlyContentBuilderInternal { NSTimer *_requestTimer; NSTimer *_minuteTimer; } +#if (TARGET_OS_IOS) + (instancetype)sharedInstance { static CountlyContentBuilderInternal *instance = nil; static dispatch_once_t onceToken; @@ -206,8 +205,5 @@ - (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSD }]; }); } -@end -#else -@implementation CountlyContentBuilderInternal -@end #endif +@end diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index 2b496eb8..77748adb 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -4,11 +4,12 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import #import NS_ASSUME_NONNULL_BEGIN +#if (TARGET_OS_IOS) typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeSlideInFromBottom, AnimationTypeSlideInFromTop, @@ -18,18 +19,17 @@ typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeIncreaseHeightFromBottom }; +#endif + @interface CountlyWebViewManager : NSObject +#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock dismissBlock:(void(^ __nullable)(void))dismissBlock; +#endif NS_ASSUME_NONNULL_END @end -#else -#import -@interface CountlyWebViewManager : NSObject -@end -#endif diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index d20d53d0..4b356df9 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -1,15 +1,18 @@ -#if (TARGET_OS_IOS) + #import "CountlyWebViewManager.h" #import "PassThroughBackgroundView.h" #import "CountlyCommon.h" //TODO: improve logging, check edge cases @interface CountlyWebViewManager() +#if (TARGET_OS_IOS) @property (nonatomic, strong) PassThroughBackgroundView *backgroundView; @property (nonatomic, copy) void (^dismissBlock)(void); +#endif @end @implementation CountlyWebViewManager +#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock @@ -241,9 +244,5 @@ - (void)closeWebView { [self.backgroundView removeFromSuperview]; }); } -@end -#else -#import "CountlyWebViewManager.h" -@implementation CountlyWebViewManager -@end #endif +@end diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h index 04df7fbc..78e79ffa 100644 --- a/PassThroughBackgroundView.h +++ b/PassThroughBackgroundView.h @@ -4,15 +4,17 @@ // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import #import #import "CountlyCommon.h" NS_ASSUME_NONNULL_BEGIN @interface PassThroughBackgroundView : UIView + +#if (TARGET_OS_IOS) @property (nonatomic, strong) WKWebView *webView; @property (nonatomic, strong) CLYButton *dismissButton; +#endif NS_ASSUME_NONNULL_END @end -#endif diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m index d7c43e09..cd8bf876 100644 --- a/PassThroughBackgroundView.m +++ b/PassThroughBackgroundView.m @@ -3,7 +3,7 @@ // This code is provided under the MIT License. // // Please visit www.count.ly for more information. -#if (TARGET_OS_IOS) + #import "PassThroughBackgroundView.h" @implementation PassThroughBackgroundView @@ -11,6 +11,7 @@ @implementation PassThroughBackgroundView @synthesize webView; - (instancetype)initWithFrame:(CGRect)frame { +#if (TARGET_OS_IOS) self = [super initWithFrame:frame]; if (self) { } @@ -28,5 +29,6 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return NO; } -@end + #endif +@end From 46a367f6add81f35032d5f12f7448253ca8710d3 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 9 Sep 2024 11:41:43 +0500 Subject: [PATCH 064/148] Added previous event name checks --- Countly.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Countly.m b/Countly.m index ff61c72a..83dff794 100644 --- a/Countly.m +++ b/Countly.m @@ -912,9 +912,10 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co key = [key cly_truncatedKey:@"Event key"]; event.PEID = previousEventID ?: @""; previousEventID = event.ID; - - filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; - previousEventName = key; + if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { + filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; + previousEventName = key; + } } event.key = key; event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; From 64ac554a87e1cd0b3967f63e7517e6cdf4c6e824 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 9 Sep 2024 11:47:29 +0500 Subject: [PATCH 065/148] Fixes iOS macros for webview and content builder --- CountlyContentBuilder.h | 2 ++ CountlyContentBuilderInternal.h | 2 ++ CountlyWebViewManager.h | 12 +++++++----- CountlyWebViewManager.m | 5 +++-- PassThroughBackgroundView.h | 13 ++++++++++--- PassThroughBackgroundView.m | 6 ++++-- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CountlyContentBuilder.h b/CountlyContentBuilder.h index 3614413a..e1b4441f 100644 --- a/CountlyContentBuilder.h +++ b/CountlyContentBuilder.h @@ -6,7 +6,9 @@ #import +#if (TARGET_OS_IOS) #import +#endif NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilder: NSObject #if (TARGET_OS_IOS) diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 905fb0b1..be18530d 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -5,7 +5,9 @@ // Please visit www.count.ly for more information. #import +#if (TARGET_OS_IOS) #import +#endif #import "CountlyCommon.h" NS_ASSUME_NONNULL_BEGIN @interface CountlyContentBuilderInternal: NSObject diff --git a/CountlyWebViewManager.h b/CountlyWebViewManager.h index 77748adb..7354aa7b 100644 --- a/CountlyWebViewManager.h +++ b/CountlyWebViewManager.h @@ -4,9 +4,11 @@ // // Please visit www.count.ly for more information. - +#if (TARGET_OS_IOS) #import #import +#endif +#import "CountlyCommon.h" NS_ASSUME_NONNULL_BEGIN #if (TARGET_OS_IOS) @@ -19,17 +21,17 @@ typedef NS_ENUM(NSUInteger, AnimationType) { AnimationTypeIncreaseHeightFromBottom }; -#endif + @interface CountlyWebViewManager : NSObject -#if (TARGET_OS_IOS) - (void)createWebViewWithURL:(NSURL *)url frame:(CGRect)frame appearBlock:(void(^ __nullable)(void))appearBlock dismissBlock:(void(^ __nullable)(void))dismissBlock; -#endif -NS_ASSUME_NONNULL_END + @end +#endif +NS_ASSUME_NONNULL_END diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index 4b356df9..ace4249f 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -4,11 +4,11 @@ #import "CountlyCommon.h" //TODO: improve logging, check edge cases -@interface CountlyWebViewManager() #if (TARGET_OS_IOS) +@interface CountlyWebViewManager() + @property (nonatomic, strong) PassThroughBackgroundView *backgroundView; @property (nonatomic, copy) void (^dismissBlock)(void); -#endif @end @implementation CountlyWebViewManager @@ -246,3 +246,4 @@ - (void)closeWebView { } #endif @end +#endif diff --git a/PassThroughBackgroundView.h b/PassThroughBackgroundView.h index 78e79ffa..c0b9b79f 100644 --- a/PassThroughBackgroundView.h +++ b/PassThroughBackgroundView.h @@ -5,16 +5,23 @@ // Please visit www.count.ly for more information. +#if (TARGET_OS_IOS) #import #import +#endif + #import "CountlyCommon.h" + NS_ASSUME_NONNULL_BEGIN +#if (TARGET_OS_IOS) @interface PassThroughBackgroundView : UIView -#if (TARGET_OS_IOS) + @property (nonatomic, strong) WKWebView *webView; @property (nonatomic, strong) CLYButton *dismissButton; -#endif -NS_ASSUME_NONNULL_END + + @end +#endif +NS_ASSUME_NONNULL_END diff --git a/PassThroughBackgroundView.m b/PassThroughBackgroundView.m index cd8bf876..d6bd8328 100644 --- a/PassThroughBackgroundView.m +++ b/PassThroughBackgroundView.m @@ -6,12 +6,13 @@ #import "PassThroughBackgroundView.h" +#if (TARGET_OS_IOS) @implementation PassThroughBackgroundView @synthesize webView; - (instancetype)initWithFrame:(CGRect)frame { -#if (TARGET_OS_IOS) + self = [super initWithFrame:frame]; if (self) { } @@ -30,5 +31,6 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return NO; } -#endif + @end +#endif From 1bdfcdadb75a5683b5e27bc3ce52addb17869477 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 9 Sep 2024 12:04:10 +0500 Subject: [PATCH 066/148] Added missing iOS macros check for content config --- CountlyConfig.h | 2 ++ CountlyConfig.m | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/CountlyConfig.h b/CountlyConfig.h index a3b6246d..dfc0397f 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -661,11 +661,13 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL enableServerConfiguration; +#if (TARGET_OS_IOS) /** * Variable to access content configurations. * @discussion Content configurations for developer to interact with SDK. */ - (CountlyContentConfig *) content; +#endif /** * This is an experimental feature and it can have breaking changes diff --git a/CountlyConfig.m b/CountlyConfig.m index 47a38968..9795ddc2 100644 --- a/CountlyConfig.m +++ b/CountlyConfig.m @@ -37,7 +37,10 @@ @implementation CountlyConfig CountlyCrashesConfig *crashes = nil; CountlySDKLimitsConfig *sdkLimitsConfig = nil; CountlyExperimentalConfig *experimental = nil; + +#if (TARGET_OS_IOS) CountlyContentConfig *content = nil; +#endif //NOTE: Device ID options NSString* const CLYDefaultDeviceID = @""; //NOTE: It will be overridden to default device ID mechanism, depending on platform. @@ -133,11 +136,13 @@ - (nonnull CountlyExperimentalConfig *)experimental { return experimental; } +#if (TARGET_OS_IOS) - (nonnull CountlyContentConfig *)content { if (content == nil) { content = CountlyContentConfig.new; } return content; } +#endif @end From 6c2be253bcd52adf3fc8fd9ea4786fc24c37629e Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 9 Sep 2024 12:41:26 +0500 Subject: [PATCH 067/148] additional checks for url --- CountlyContentBuilderInternal.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 4a0ef01b..9af91e28 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -105,8 +105,9 @@ - (void)fetchContents { NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSString *pathToHtml = jsonResponse[@"pathToHtml"]; NSDictionary *placementCoordinates = jsonResponse[@"placementCoordinates"]; - - [self showContentWithHtmlPath:pathToHtml placementCoordinates:placementCoordinates]; + if(pathToHtml) { + [self showContentWithHtmlPath:pathToHtml placementCoordinates:placementCoordinates]; + } self->_isRequestQueueLocked = NO; }]; @@ -162,9 +163,15 @@ - (NSString *)resolutionJson { return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; } -- (void)showContentWithHtmlPath:(NSString *)pathToHtml placementCoordinates:(NSDictionary *)placementCoordinates { +- (void)showContentWithHtmlPath:(NSString *)urlString placementCoordinates:(NSDictionary *)placementCoordinates { // Convert pathToHtml to NSURL - NSURL *url = [NSURL URLWithString:pathToHtml]; + NSURL *url = [NSURL URLWithString:urlString]; + + if (!url || !url.scheme || !url.host) { + NSLog(@"The URL is not valid: %@", urlString); + return; + } + dispatch_async(dispatch_get_main_queue(), ^ { // Detect screen orientation From 99e225e6297ef189bff9080a12c2d3a37ea61547 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 17 Sep 2024 13:31:39 +0300 Subject: [PATCH 068/148] feat: build action --- .github/workflows/build.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..25a396f0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: Build for iOS, MacOS, TvOS, and WatchOS + +permissions: + checks: write + pull-requests: write + +on: + push: + branches: + - master + - staging + pull_request: + branches: + - master + - staging +jobs: + build: + runs-on: macos-latest + strategy: + matrix: + sdk: [macos, iphoneos, tvos, watchos] + steps: + # To check the github context + - name: Checkout SDK code + uses: actions/checkout@v4 + + - name: Build for ${{ matrix.sdk }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} From 5e6d9aa69d2d147d5c5b4f3542b54e4eacb49b9e Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 17 Sep 2024 15:56:40 +0300 Subject: [PATCH 069/148] chore: show sdks --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25a396f0..5ed8fb99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,9 @@ jobs: sdk: [macos, iphoneos, tvos, watchos] steps: # To check the github context + - name: Show SDK list + uses: xcodebuild -showsdks + - name: Checkout SDK code uses: actions/checkout@v4 From 1ba22ead62ea90fe74fe43f8ea46c500af35a0f6 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 17 Sep 2024 15:59:39 +0300 Subject: [PATCH 070/148] chore: show sdks --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ed8fb99..1174f7d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: steps: # To check the github context - name: Show SDK list - uses: xcodebuild -showsdks + run: xcodebuild -showsdks - name: Checkout SDK code uses: actions/checkout@v4 From 69308c13382e0064f593bc03db11818d35e39dbf Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 17 Sep 2024 16:01:54 +0300 Subject: [PATCH 071/148] fix: correct platforms --- .github/workflows/build.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1174f7d1..1a0465d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,9 @@ jobs: runs-on: macos-latest strategy: matrix: - sdk: [macos, iphoneos, tvos, watchos] + sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] steps: # To check the github context - - name: Show SDK list - run: xcodebuild -showsdks - - name: Checkout SDK code uses: actions/checkout@v4 From 44cedd24a9c5c22b5e9166bdad9674ef0a562bb1 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 17 Sep 2024 16:03:31 +0300 Subject: [PATCH 072/148] fix: correct name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a0465d9..54941978 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build for iOS, MacOS, TvOS, and WatchOS +name: Build the SDK permissions: checks: write From 03e8ce047862785f8c4c82fea99cc096a165736a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 23 Sep 2024 19:26:17 +0500 Subject: [PATCH 073/148] Added current view name in event segmentation --- Countly.m | 1 + CountlyViewTrackingInternal.h | 2 -- CountlyViewTrackingInternal.m | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Countly.m b/Countly.m index 83dff794..d6e46173 100644 --- a/Countly.m +++ b/Countly.m @@ -915,6 +915,7 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { filteredSegmentations[kCountlyPreviousEventName] = previousEventName ?: @""; previousEventName = key; + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; } } event.key = key; diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index cc5ff653..47f95371 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -8,7 +8,6 @@ extern NSString* const kCountlyReservedEventView; -//TODO: Need discussion for its usage and then we decide to keep it or remove it extern NSString* const kCountlyCurrentView; extern NSString* const kCountlyPreviousView; extern NSString* const kCountlyPreviousEventName; @@ -19,7 +18,6 @@ extern NSString* const kCountlyPreviousEventName; @property (nonatomic) NSString* previousViewID; @property (nonatomic) BOOL enablePreviousNameRecording; -//TODO: Need discussion for its usage and then we decide to keep it or remove it @property (nonatomic) NSString* currentViewName; @property (nonatomic) NSString* previousViewName; diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index defeab6a..17c50dc2 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -17,7 +17,6 @@ @interface CountlyViewTrackingInternal () NSString* const kCountlyReservedEventView = @"[CLY]_view"; -//TODO: Need discussion for its usage and then we decide to keep it or remove it NSString* const kCountlyCurrentView = @"cly_cvn"; NSString* const kCountlyPreviousView = @"cly_pvn"; From 0541c0aa3c57969fce7dbe6fb0cde97381b5a8b2 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 23 Sep 2024 19:34:05 +0500 Subject: [PATCH 074/148] Added changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eafa1d47..81971fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 24.7.3 +* Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) + ## 24.7.2 * Automatic view pause/resumes are changed with stop/start for better data consistency. * Added the config interface 'experimental' to group experimental features. From 27747d7a2ad86288ea9ac50e3efdf57304a59dee Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 23 Sep 2024 19:36:17 +0500 Subject: [PATCH 075/148] Updated SDK version to 24.7.3 --- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 4a9d537c..37cf21a3 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.2' + s.version = '24.7.3' 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' diff --git a/Countly.podspec b/Countly.podspec index 7650edfc..17ed0ab4 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.2' + s.version = '24.7.3' 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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d29acd28..1394aab3 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -726,7 +726,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.2; + MARKETING_VERSION = 24.7.3; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -758,7 +758,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.2; + MARKETING_VERSION = 24.7.3; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index 1ec2564f..f7564a6c 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -29,7 +29,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.2"; +NSString* const kCountlySDKVersion = @"24.7.3"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; From 059b1dd9f37a8ddfdf19101d7d22ccd0d65d640c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 13:53:25 +0300 Subject: [PATCH 076/148] feat. more targets for min --- .github/workflows/build.yml | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54941978..34992630 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: runs-on: macos-latest strategy: matrix: - sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] + sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5, iphoneos10.0, macosx10.14, appletvos10.0, watchos4.0] steps: # To check the github context - name: Checkout SDK code diff --git a/.gitignore b/.gitignore index 4c22dfa8..eb632e21 100755 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ Thumbs.db Countly.xcodeproj/project.xcworkspace/ Countly.xcodeproj/xcuserdata/ .build +build .swiftpm \ No newline at end of file From 485bf61bda4b627d9f26f57096dd9c78575acfee Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 13:57:16 +0300 Subject: [PATCH 077/148] feat. more targets for min --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34992630..54941978 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: runs-on: macos-latest strategy: matrix: - sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5, iphoneos10.0, macosx10.14, appletvos10.0, watchos4.0] + sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] steps: # To check the github context - name: Checkout SDK code From 2e453e9a8184fbbb6028c5df85fc17382a73b5a8 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:16:09 +0300 Subject: [PATCH 078/148] feat: show sdk list --- .github/workflows/build.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54941978..7907b110 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,14 +15,8 @@ on: - staging jobs: build: - runs-on: macos-latest - strategy: - matrix: - sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] + runs-on: macos-12 steps: # To check the github context - - name: Checkout SDK code - uses: actions/checkout@v4 - - - name: Build for ${{ matrix.sdk }} - run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} + - name: Show SDK list + run: xcodebuild -showsdks From ecb4ac815bd0ebf44e1f113dcfe40d62226e05eb Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:26:30 +0300 Subject: [PATCH 079/148] feat: target to thing --- .github/workflows/build.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7907b110..9bf80c4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,24 @@ on: - staging jobs: build: - runs-on: macos-12 + runs-on: macos-latest + strategy: + matrix: + sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] + target: [macosx10.14, iphoneos10.0, appletvos10.0, watchos4.0] + include: + - sdk: macosx14.5 + target: macosx10.14 + - sdk: iphoneos17.5 + target: iphoneos10.0 + - sdk: appletvos17.5 + target: appletvos10.0 + - sdk: watchos10.5 + target: watchos4.0 steps: # To check the github context - - name: Show SDK list - run: xcodebuild -showsdks + - name: Checkout SDK code + uses: actions/checkout@v4 + + - name: Build for ${{ matrix.sdk }} target ${{ matrix.target }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} -target ${{ matrix.target }} From 77418b2f76d7288deba3cd43e8d4e893c5b67bfc Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:29:13 +0300 Subject: [PATCH 080/148] feat: target to thing --- .github/workflows/build.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bf80c4a..f6115fe7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,15 +20,31 @@ jobs: matrix: sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] target: [macosx10.14, iphoneos10.0, appletvos10.0, watchos4.0] - include: + exclude: - sdk: macosx14.5 + target: iphoneos10.0 + - sdk: macosx14.5 + target: appletvos10.0 + - sdk: macosx14.5 + target: watchos4.0 + - sdk: iphoneos17.5 target: macosx10.14 - sdk: iphoneos17.5 + target: appletvos10.0 + - sdk: iphoneos17.5 + target: watchos4.0 + - sdk: appletvos17.5 + target: macosx10.14 + - sdk: appletvos17.5 target: iphoneos10.0 - sdk: appletvos17.5 - target: appletvos10.0 - - sdk: watchos10.5 target: watchos4.0 + - sdk: watchos10.5 + target: macosx10.14 + - sdk: watchos10.5 + target: iphoneos10.0 + - sdk: watchos10.5 + target: appletvos10.0 steps: # To check the github context - name: Checkout SDK code From bb0895eeb9be3420bfc8ab98bfbda1687f983994 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:33:39 +0300 Subject: [PATCH 081/148] feat: target to thing --- .github/workflows/build.yml | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6115fe7..e3baeed6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,32 +19,7 @@ jobs: strategy: matrix: sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] - target: [macosx10.14, iphoneos10.0, appletvos10.0, watchos4.0] - exclude: - - sdk: macosx14.5 - target: iphoneos10.0 - - sdk: macosx14.5 - target: appletvos10.0 - - sdk: macosx14.5 - target: watchos4.0 - - sdk: iphoneos17.5 - target: macosx10.14 - - sdk: iphoneos17.5 - target: appletvos10.0 - - sdk: iphoneos17.5 - target: watchos4.0 - - sdk: appletvos17.5 - target: macosx10.14 - - sdk: appletvos17.5 - target: iphoneos10.0 - - sdk: appletvos17.5 - target: watchos4.0 - - sdk: watchos10.5 - target: macosx10.14 - - sdk: watchos10.5 - target: iphoneos10.0 - - sdk: watchos10.5 - target: appletvos10.0 + target: [macosx13.0, iphoneos16.1, appletvos10.0, watchos4.0] steps: # To check the github context - name: Checkout SDK code From 0b215148a5de8b56d14a5e524c0ea2013a6c5816 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:35:03 +0300 Subject: [PATCH 082/148] feat: target to thing --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3baeed6..a48dce06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,8 @@ jobs: runs-on: macos-latest strategy: matrix: - sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] - target: [macosx13.0, iphoneos16.1, appletvos10.0, watchos4.0] + sdk: [macosx14.5] + target: [macosx13.0] steps: # To check the github context - name: Checkout SDK code From 5e69c48d53f36a1568c13655a49a70a9942581e8 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 14:37:50 +0300 Subject: [PATCH 083/148] feat: target to thing --- .github/workflows/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a48dce06..54941978 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,11 @@ jobs: runs-on: macos-latest strategy: matrix: - sdk: [macosx14.5] - target: [macosx13.0] + sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] steps: # To check the github context - name: Checkout SDK code uses: actions/checkout@v4 - - name: Build for ${{ matrix.sdk }} target ${{ matrix.target }} - run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} -target ${{ matrix.target }} + - name: Build for ${{ matrix.sdk }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} From 1370ecdc714ff1b500fe4511886d186708eaeaa7 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 15:09:46 +0300 Subject: [PATCH 084/148] feat: give specific runner --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54941978..0affa57b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ on: - staging jobs: build: - runs-on: macos-latest + runs-on: macos-14 strategy: matrix: sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] From 0a8db61dd69f02a72b95a0abf62d7644ab413106 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 15:57:49 +0300 Subject: [PATCH 085/148] feat: show sdks --- .github/workflows/build.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0affa57b..276f615d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,14 +15,12 @@ on: - staging jobs: build: - runs-on: macos-14 + runs-on: macos-${{ matrix.os_version }} strategy: matrix: - sdk: [macosx14.5, iphoneos17.5, appletvos17.5, watchos10.5] + os_version: [12,13,14] + steps: - # To check the github context - - name: Checkout SDK code - uses: actions/checkout@v4 - - name: Build for ${{ matrix.sdk }} - run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.sdk }} + - name: Build for ${{ matrix.os_version }} + run: xcodebuild -showsdks From be940e75808bae41742be68d597f142681fcade9 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 16:03:56 +0300 Subject: [PATCH 086/148] feat: more versions --- .github/workflows/build.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 276f615d..59187525 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,12 +15,14 @@ on: - staging jobs: build: - runs-on: macos-${{ matrix.os_version }} + runs-on: macos-${{ matrix.env.os_version }} strategy: matrix: - os_version: [12,13,14] - + env: [{os_version: 14, sdk: "macosx14.5"},{os_version: 14, sdk: "iphoneos17.5"},{os_version: 14, sdk: "appletvos17.5"},{os_version: 14, sdk: "watchos10.5"},{os_version: 12, sdk: "iphoneos16.2"},{os_version: 12, sdk: "macosx13.1"},{os_version: 12, sdk: "appletvos16.1"},{os_version: 12, sdk: "watchos9.1"},{os_version: 13, sdk: "iphoneos17.2"},{os_version: 13, sdk: "macosx14.2"},{os_version: 13, sdk: "appletvos17.2"},{os_version: 13, sdk: "watchos10.2"}] steps: + # To check the github context + - name: Checkout SDK code + uses: actions/checkout@v4 - - name: Build for ${{ matrix.os_version }} - run: xcodebuild -showsdks + - name: Build for ${{ matrix.env.sdk }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} From d9875cc8699ba9f826f9f1652fc12ea0b8e72b2f Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 16:15:45 +0300 Subject: [PATCH 087/148] feat: run tests --- .github/workflows/run_tests.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..a6c3f3cf --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,30 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: Swift + +on: [push] + +jobs: + build: + name: Swift ${{ matrix.swift }} on ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + swift: ["5.2", "5.3"] + runs-on: ${{ matrix.os }} + steps: + - uses: swift-actions/setup-swift@65540b95f51493d65f5e59e97dcef9629ddf11bf + with: + swift-version: ${{ matrix.swift }} + - uses: actions/checkout@v4 + - name: Build + run: swift build + - name: Run tests + run: swift test \ No newline at end of file From beb2b2055b613e5ebb4d072a3b02b4a2eab0a367 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 16:25:21 +0300 Subject: [PATCH 088/148] feat: run tests --- .github/workflows/build.yml | 2 +- .github/workflows/run_tests.yml | 38 ++++++++++++++------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59187525..0a98b3ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,4 +25,4 @@ jobs: uses: actions/checkout@v4 - name: Build for ${{ matrix.env.sdk }} - run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index a6c3f3cf..1c8a43a3 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,30 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +name: Build the SDK with tests -# GitHub recommends pinning actions to a commit SHA. -# To get a newer version, you will need to update the SHA. -# You can also reference a tag or branch, but the action may change without warning. +permissions: + checks: write + pull-requests: write -name: Swift - -on: [push] +on: + push: + branches: + - master + - staging + pull_request: + branches: + - master + - staging jobs: build: - name: Swift ${{ matrix.swift }} on ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - swift: ["5.2", "5.3"] - runs-on: ${{ matrix.os }} + name: Run test + runs-on: macos-latest steps: - - uses: swift-actions/setup-swift@65540b95f51493d65f5e59e97dcef9629ddf11bf - with: - swift-version: ${{ matrix.swift }} - uses: actions/checkout@v4 - name: Build - run: swift build - - name: Run tests - run: swift test \ No newline at end of file + run: xcodebuild -project Countly.xcodeproj -scheme CountlyTests -sdk macosx14.5 test \ No newline at end of file From 9be6249addb54dacd2b930e9804d026fcd891171 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 16:33:11 +0300 Subject: [PATCH 089/148] feat: development team --- Countly.xcodeproj/project.pbxproj | 2 ++ CountlyTests/CountlyDeviceIDTests.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d29acd28..ff9ebcf7 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -537,6 +537,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3S32KFMHB8; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; @@ -561,6 +562,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3S32KFMHB8; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; diff --git a/CountlyTests/CountlyDeviceIDTests.swift b/CountlyTests/CountlyDeviceIDTests.swift index 208e9685..1e659c5d 100644 --- a/CountlyTests/CountlyDeviceIDTests.swift +++ b/CountlyTests/CountlyDeviceIDTests.swift @@ -121,7 +121,7 @@ class CountlyDeviceIDTests: CountlyBaseTestCase { #else var UUID = CountlyPersistency.sharedInstance().retrieveNSUUID() if UUID == nil { - UUID = UUID().uuidString + UUID = NSUUID().uuidString CountlyPersistency.sharedInstance().storeNSUUID(UUID) } From 5f7f42fba23d9343ae64289ca93f84e4301e4edf Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 25 Sep 2024 16:38:47 +0300 Subject: [PATCH 090/148] revert: other changes --- .github/workflows/run_tests.yml | 24 ------------------------ Countly.xcodeproj/project.pbxproj | 2 -- CountlyTests/CountlyDeviceIDTests.swift | 2 +- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml deleted file mode 100644 index 1c8a43a3..00000000 --- a/.github/workflows/run_tests.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Build the SDK with tests - -permissions: - checks: write - pull-requests: write - -on: - push: - branches: - - master - - staging - pull_request: - branches: - - master - - staging - -jobs: - build: - name: Run test - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Build - run: xcodebuild -project Countly.xcodeproj -scheme CountlyTests -sdk macosx14.5 test \ No newline at end of file diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index ff9ebcf7..d29acd28 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -537,7 +537,6 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 3S32KFMHB8; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; @@ -562,7 +561,6 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 3S32KFMHB8; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; diff --git a/CountlyTests/CountlyDeviceIDTests.swift b/CountlyTests/CountlyDeviceIDTests.swift index 1e659c5d..208e9685 100644 --- a/CountlyTests/CountlyDeviceIDTests.swift +++ b/CountlyTests/CountlyDeviceIDTests.swift @@ -121,7 +121,7 @@ class CountlyDeviceIDTests: CountlyBaseTestCase { #else var UUID = CountlyPersistency.sharedInstance().retrieveNSUUID() if UUID == nil { - UUID = NSUUID().uuidString + UUID = UUID().uuidString CountlyPersistency.sharedInstance().storeNSUUID(UUID) } From 71b7d199453bbf239679811e4dd6de5ce78099d2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 26 Sep 2024 13:18:50 +0300 Subject: [PATCH 091/148] feat: test step --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a98b3ae..0376c428 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,5 +24,8 @@ jobs: - name: Checkout SDK code uses: actions/checkout@v4 - - name: Build for ${{ matrix.env.sdk }} - run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} \ No newline at end of file + - name: Build for the sdk ${{ matrix.env.sdk }} on macOS ${{ matrix.env.os_version }} + run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} + + - name: Run the tests for the sdk ${{ matrix.env.sdk }} on macOS ${{ matrix.env.os_version }} + run: xcodebuild -project Countly.xcodeproj test -sdk ${{ matrix.env.sdk }} -scheme CountlyTests \ No newline at end of file From 67988d80fedc8f045643618e60de38fdc29c85d5 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 26 Sep 2024 13:42:06 +0300 Subject: [PATCH 092/148] feat: destionsin --- .github/workflows/build.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0376c428..3c5daf8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,8 +17,23 @@ jobs: build: runs-on: macos-${{ matrix.env.os_version }} strategy: + fail-fast: false matrix: - env: [{os_version: 14, sdk: "macosx14.5"},{os_version: 14, sdk: "iphoneos17.5"},{os_version: 14, sdk: "appletvos17.5"},{os_version: 14, sdk: "watchos10.5"},{os_version: 12, sdk: "iphoneos16.2"},{os_version: 12, sdk: "macosx13.1"},{os_version: 12, sdk: "appletvos16.1"},{os_version: 12, sdk: "watchos9.1"},{os_version: 13, sdk: "iphoneos17.2"},{os_version: 13, sdk: "macosx14.2"},{os_version: 13, sdk: "appletvos17.2"},{os_version: 13, sdk: "watchos10.2"}] + env: + [ + {os_version: 14, sdk: "macosx14.5"}, + {os_version: 14, sdk: "iphoneos17.5"}, + {os_version: 14, sdk: "appletvos17.5"}, + {os_version: 14, sdk: "watchos10.5"}, + {os_version: 12, sdk: "iphoneos16.2"}, + {os_version: 12, sdk: "macosx13.1"}, + {os_version: 12, sdk: "appletvos16.1"}, + {os_version: 12, sdk: "watchos9.1"}, + {os_version: 13, sdk: "iphoneos17.2"}, + {os_version: 13, sdk: "macosx14.2"}, + {os_version: 13, sdk: "appletvos17.2"}, + {os_version: 13, sdk: "watchos10.2"} + ] steps: # To check the github context - name: Checkout SDK code @@ -28,4 +43,4 @@ jobs: run: xcodebuild -project Countly.xcodeproj build -sdk ${{ matrix.env.sdk }} - name: Run the tests for the sdk ${{ matrix.env.sdk }} on macOS ${{ matrix.env.os_version }} - run: xcodebuild -project Countly.xcodeproj test -sdk ${{ matrix.env.sdk }} -scheme CountlyTests \ No newline at end of file + run: echo good #xcodebuild -project Countly.xcodeproj test -destination ${{ matrix.env.destination }} -scheme CountlyTests \ No newline at end of file From a4c4b825d5980ff2c253a6699972b66bff1e12f6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 2 Oct 2024 18:18:58 +0500 Subject: [PATCH 093/148] Update content request and response with new model changes --- CountlyContentBuilderInternal.m | 34 +++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 9af91e28..21eae28b 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -102,9 +102,23 @@ - (void)fetchContents { return; } - NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *pathToHtml = jsonResponse[@"pathToHtml"]; - NSDictionary *placementCoordinates = jsonResponse[@"placementCoordinates"]; + NSError *jsonError; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) { + CLY_LOG_I(@"Failed to parse JSON: %@", jsonError); + self->_isRequestQueueLocked = NO; + return; + } + + if (!jsonResponse) { + CLY_LOG_I(@"Received empty or null response."); + self->_isRequestQueueLocked = NO; + return; + } + + NSString *pathToHtml = jsonResponse[@"html"]; + NSDictionary *placementCoordinates = jsonResponse[@"geo"]; if(pathToHtml) { [self showContentWithHtmlPath:pathToHtml placementCoordinates:placementCoordinates]; } @@ -119,8 +133,8 @@ - (NSURLRequest *)fetchContentsRequest NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; NSString *resolutionJson = [self resolutionJson]; queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", - kCountlyQSKeyMethod, kCountlyCBFetchContent, - @"res", resolutionJson]; + @"app_id", @"66fa992b8757e0f5c3a52cfb", + @"resolution", resolutionJson]; queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; @@ -155,8 +169,8 @@ - (NSString *)resolutionJson { CGFloat height = screenBounds.size.height; NSDictionary *resolutionDict = @{ - @"p": @{@"h": @(height), @"w": @(width)}, - @"l": @{@"h": @(width), @"w": @(height)} + @"portrait": @{@"height": @(height), @"width": @(width)}, + @"landscape": @{@"height": @(width), @"width": @(height)} }; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resolutionDict options:0 error:nil]; @@ -180,12 +194,12 @@ - (void)showContentWithHtmlPath:(NSString *)urlString placementCoordinates:(NSDi // Get the appropriate coordinates based on the orientation - NSDictionary *coordinates = isLandscape ? placementCoordinates[@"landscape"] : placementCoordinates[@"portrait"]; + NSDictionary *coordinates = isLandscape ? placementCoordinates[@"l"] : placementCoordinates[@"p"]; CGFloat x = [coordinates[@"x"] floatValue]; CGFloat y = [coordinates[@"y"] floatValue]; - CGFloat width = [coordinates[@"width"] floatValue]; - CGFloat height = [coordinates[@"height"] floatValue]; + CGFloat width = [coordinates[@"w"] floatValue]; + CGFloat height = [coordinates[@"h"] floatValue]; CGRect frame = CGRectMake(x, y, width, height); From 82e0b1ae2bb194821b968c557ef641f832e3ebdb Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 2 Oct 2024 18:20:04 +0500 Subject: [PATCH 094/148] Removed app_id from request --- CountlyContentBuilderInternal.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 21eae28b..59589114 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -132,8 +132,7 @@ - (NSURLRequest *)fetchContentsRequest { NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; NSString *resolutionJson = [self resolutionJson]; - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", - @"app_id", @"66fa992b8757e0f5c3a52cfb", + queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"resolution", resolutionJson]; queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; From fd4d8273b5a8d7cd36699f28656d4224d3ab6816 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 3 Oct 2024 23:13:56 +0500 Subject: [PATCH 095/148] some safe checks when searlizing events --- CountlyPersistency.m | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CountlyPersistency.m b/CountlyPersistency.m index 276f70db..5edff471 100644 --- a/CountlyPersistency.m +++ b/CountlyPersistency.m @@ -282,23 +282,27 @@ - (void)recordEvent:(CountlyEvent *)event - (NSString *)serializedRecordedEvents { - NSMutableArray* tempArray = NSMutableArray.new; - + NSMutableArray *tempArray = NSMutableArray.new; + @synchronized (self.recordedEvents) { if (self.recordedEvents.count == 0) return nil; - - for (CountlyEvent* event in self.recordedEvents.copy) + + NSArray *eventsCopy = self.recordedEvents.copy; + + for (CountlyEvent *event in eventsCopy) { [tempArray addObject:[event dictionaryRepresentation]]; - [self.recordedEvents removeObject:event]; } + + [self.recordedEvents removeObjectsInArray:eventsCopy]; } - + return [tempArray cly_JSONify]; } + - (void)flushEvents { @synchronized (self.recordedEvents) From fdd1d3a0fc3c9e7f1ca7ea13e13ee399774c8361 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 4 Oct 2024 12:52:37 +0500 Subject: [PATCH 096/148] Added changelog entry for content feature changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81971fa0..45f56a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) +* Updated the SDK to ensure compatibility with the latest server response models ## 24.7.2 * Automatic view pause/resumes are changed with stop/start for better data consistency. From 3ccee78a82e9d205fc2b0b217e3beb7f5af724e5 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 14 Oct 2024 12:35:13 +0500 Subject: [PATCH 097/148] Some fixes for view duration and added tests --- Countly.m | 4 +- Countly.xcodeproj/project.pbxproj | 4 + CountlyTests/CountlyViewTests.swift | 199 ++++++++++++++++++++++++++++ CountlyViewData.h | 2 +- CountlyViewData.m | 7 +- CountlyViewTrackingInternal.m | 8 +- 6 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 CountlyTests/CountlyViewTests.swift diff --git a/Countly.m b/Countly.m index d6e46173..f97c7629 100644 --- a/Countly.m +++ b/Countly.m @@ -400,13 +400,13 @@ - (void)suspend isSuspended = YES; + [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; if (!CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance endSession]; - [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; - [CountlyPersistency.sharedInstance saveToFile]; } diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 1394aab3..ac965640 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 3961C6BA2C6633C000DD38BA /* CountlyWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */; }; 3964A3E72C2AF8E90091E677 /* CountlySegmentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */; }; 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */; }; + 3969D0232CB80848000F8A32 /* CountlyViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */; }; 3972EDDB2C08A38D00EB9D3E /* CountlyEventStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */; }; 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */; }; 399117D12C69F73D00DC4C66 /* CountlyContentBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */; }; @@ -131,6 +132,7 @@ 3961C6B42C6633C000DD38BA /* CountlyWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyWebViewManager.m; sourceTree = ""; }; 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlySegmentationTests.swift; sourceTree = ""; }; 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyDeviceIDTests.swift; sourceTree = ""; }; + 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyViewTests.swift; sourceTree = ""; }; 3972EDDA2C08A38D00EB9D3E /* CountlyEventStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyEventStruct.swift; sourceTree = ""; }; 3979E47C2C0760E900FA1CA4 /* CountlyUserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyUserProfileTests.swift; sourceTree = ""; }; 399117CD2C69F73D00DC4C66 /* CountlyContentBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyContentBuilder.m; sourceTree = ""; }; @@ -217,6 +219,7 @@ 3966DBCE2C11EE270002ED97 /* CountlyDeviceIDTests.swift */, 3964A3E62C2AF8E90091E677 /* CountlySegmentationTests.swift */, 399B464F2C52813700AD384E /* CountlyLocationTests.swift */, + 3969D0222CB80848000F8A32 /* CountlyViewTests.swift */, ); path = CountlyTests; sourceTree = ""; @@ -463,6 +466,7 @@ buildActionMask = 2147483647; files = ( 1A5C4C972B35B0850032EE1F /* CountlyTests.swift in Sources */, + 3969D0232CB80848000F8A32 /* CountlyViewTests.swift in Sources */, 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */, 1A50D7052B3C5AA3009C6938 /* CountlyBaseTestCase.swift in Sources */, 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */, diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift new file mode 100644 index 00000000..54f7c1de --- /dev/null +++ b/CountlyTests/CountlyViewTests.swift @@ -0,0 +1,199 @@ +// +// CountlyViewTrackingTests.swift +// CountlyTests +// +// Copyright © 2024 Countly. All rights reserved. +// + +import XCTest +@testable import Countly + +class CountlyViewTrackingTests: CountlyBaseTestCase { + + func checkPersistentValues() { + let countlyPersistency = CountlyPersistency.sharedInstance() + if(countlyPersistency != nil) { + if let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? NSMutableArray, + let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? NSMutableArray, + let startedEvents = CountlyPersistency.sharedInstance().value(forKey: "startedEvents") as? NSMutableDictionary, + let isQueueBeingModified = CountlyPersistency.sharedInstance().value(forKey: "isQueueBeingModified") as? Bool { + print("Successfully access private properties.") + + + } + else { + print("Failed to access private properties.") + } + } + + } + + func testViewForegroundBackground() { + let config = createBaseConfig() + // No Device ID provided during init + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") + DispatchQueue.global().asyncAfter(deadline: .now() + 10) { // Delayed by 10 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeViewExpectation.fulfill() + } + + let bgExpectation = XCTestExpectation(description: "Wait for background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 15) { // Delayed by 15 seconds + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation.fulfill() + } + + let fgExpectation = XCTestExpectation(description: "Wait for active notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 20) { // Delayed by 20 seconds + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation.fulfill() + } + + let bgExpectation1 = XCTestExpectation(description: "Wait for second background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 25) { // Delayed by 25 seconds + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation1.fulfill() + } + + let fgExpectation1 = XCTestExpectation(description: "Wait for second active notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 30) { // Delayed by 30 seconds + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation1.fulfill() + } + + // Wait for all expectations or timeout + wait(for: [pauseViewExpectation, resumeViewExpectation, bgExpectation, fgExpectation, bgExpectation1, fgExpectation1], timeout: 35) + + let viewID1 = Countly.sharedInstance().views().startView("startView") + + checkPersistentValues() + Countly.sharedInstance().views().stopAllViews(nil); + + checkPersistentValues() + } + + + func testViewTrackingInit_CNR_AV() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + wait(for: [pauseViewExpectation], timeout: 10) + } + + func testViewTrackingInit_CR_CNG_AV() throws { + let config = createBaseConfig() + config.requiresConsent = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithoutConsent") + XCTAssertNil(viewID, "Auto-stopped view should not be started when consent is not given.") + } + + func testViewTrackingInit_CR_CGV_AV() throws { + let config = createBaseConfig() + config.requiresConsent = true + config.consents = [CLYConsent.viewTracking] + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithConsent") + XCTAssertNotNil(viewID, "Auto-stopped view should be started when view tracking consent is given.") + } + + func testManualViewTrackingInit_CNR_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualView") + XCTAssertNotNil(viewID, "Manual view should be started successfully.") + + Countly.sharedInstance().views().stopView(withID: viewID) + } + + func testManualViewTrackingInit_CR_CNG_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualViewWithoutConsent") + XCTAssertNil(viewID, "Manual view should not be started when consent is not given.") + } + + func testManualViewTrackingInit_CR_CGV_MV() throws { + let config = createBaseConfig() + config.manualSessionHandling = true + config.requiresConsent = true + config.consents = [CLYConsent.viewTracking] + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestManualViewWithConsent") + XCTAssertNotNil(viewID, "Manual view should be started when view tracking consent is given.") + + Countly.sharedInstance().views().stopView(withID: viewID) + } + + func testPauseAndResumeViewTracking() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestViewPauseResume") + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + + let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") + DispatchQueue.global().asyncAfter(deadline: .now() + 2) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseViewExpectation.fulfill() + } + + let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") + DispatchQueue.global().asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeViewExpectation.fulfill() + } + + wait(for: [pauseViewExpectation, resumeViewExpectation], timeout: 10) + } + + func testViewTrackingWithBackgroundAndForegroundNotifications() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("TestViewNotifications") + + let bgExpectation = XCTestExpectation(description: "Wait for background notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + bgExpectation.fulfill() + } + + let fgExpectation = XCTestExpectation(description: "Wait for foreground notification") + DispatchQueue.global().asyncAfter(deadline: .now() + 10) { + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + fgExpectation.fulfill() + } + + wait(for: [bgExpectation, fgExpectation], timeout: 15) + XCTAssertNotNil(viewID, "View should handle background and foreground notifications correctly.") + } +} diff --git a/CountlyViewData.h b/CountlyViewData.h index d56dc687..e2f40f18 100644 --- a/CountlyViewData.h +++ b/CountlyViewData.h @@ -60,7 +60,7 @@ * Duration of the view * @discussion it returns the duration of view in foreground after view started. */ -- (NSTimeInterval)duration; +- (NSInteger)duration; /** diff --git a/CountlyViewData.m b/CountlyViewData.m index 28ddb6f3..ff8b58d0 100644 --- a/CountlyViewData.m +++ b/CountlyViewData.m @@ -23,17 +23,18 @@ - (instancetype)initWithID:(NSString *)viewID viewName:(NSString *)viewName return self; } -- (NSTimeInterval)duration +- (NSInteger)duration { NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - self.viewStartTime; - return duration; + return (NSInteger)round(duration); // Rounds to the nearest integer, to fix long value converted to 0 on server side. } - (void)pauseView { if (self.viewStartTime) { - self.viewStartTime = 0; + // For safe side we have set the value to current time stamp instead of 0 when pausing the view, as setting it to 0 could result in an invalid duration value. + self.viewStartTime = CountlyCommon.sharedInstance.uniqueTimestamp; } } diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 17c50dc2..89af7504 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -377,10 +377,10 @@ - (void)stopViewWithIDInternal:(NSString *) viewKey customSegmentation:(NSDictio segmentation[kCountlyVTKeyName] = viewData.viewName; segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; - NSTimeInterval duration = viewData.duration; + NSInteger duration = viewData.duration; [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventView segmentation:segmentation count:1 sum:0 duration:duration ID:viewData.viewID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; - CLY_LOG_D(@"%s View tracking ended: %@ duration: %.17g", __FUNCTION__, viewData.viewName, duration); + CLY_LOG_D(@"%s View tracking ended: %@ duration: %ld", __FUNCTION__, viewData.viewName, (long)duration); if (!autoPaused) { [self.viewDataDictionary removeObjectForKey:viewKey]; } @@ -511,7 +511,7 @@ -(CountlyViewData* ) currentView - (void)stopAutoStoppedView { CountlyViewData* currentView = self.currentView; - if (currentView && currentView.isAutoStoppedView) + if (currentView && currentView.isAutoStoppedView && !currentView.willStartAgain) { [self stopViewWithIDInternal:self.currentView.viewID customSegmentation:nil]; } @@ -537,8 +537,8 @@ - (void)stopRunningViewsInternal - (void)pauseViewInternal:(CountlyViewData*) viewData { - [viewData pauseView]; [self stopViewWithIDInternal:viewData.viewID customSegmentation:nil autoPaused:YES]; + [viewData pauseView]; } - (void)startStoppedViewsInternal From ff7e99bb958f617a2cbd8c4bcbeee5096b9ce3a6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 16 Oct 2024 12:21:03 +0500 Subject: [PATCH 098/148] Added new functions to ease the presenting the feedback widgets --- CountlyFeedbacks.h | 9 ++++++ CountlyFeedbacks.m | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/CountlyFeedbacks.h b/CountlyFeedbacks.h index dd2595e4..48d2aeab 100644 --- a/CountlyFeedbacks.h +++ b/CountlyFeedbacks.h @@ -26,6 +26,15 @@ extern NSString* const kCountlyReservedEventStarRating; - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; +- (void) presentNPS; +- (void) presentNPS:(NSString *)nameIDorTag; + +- (void) presentSurvey; +- (void) presentSurvey:(NSString *)nameIDorTag; + +- (void) presentRating; +- (void) presentRating:(NSString *)nameIDorTag; + @property (nonatomic) NSString* message; @property (nonatomic) NSString* dismissButtonTitle; @property (nonatomic) NSUInteger sessionCount; diff --git a/CountlyFeedbacks.m b/CountlyFeedbacks.m index 44fff2ba..25fb4bcb 100644 --- a/CountlyFeedbacks.m +++ b/CountlyFeedbacks.m @@ -507,5 +507,78 @@ - (NSURLRequest *)feedbacksRequest } } +- (void)presentNPS { + [self presentNPS:nil]; +} + +- (void)presentNPS:(NSString *)nameIDorTag { + CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@", nameIDorTag); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag]; +} + +- (void)presentSurvey { + [self presentSurvey:nil]; +} + +- (void)presentSurvey:(NSString *)nameIDorTag { + CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@", nameIDorTag); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag]; +} + +- (void)presentRating { + [self presentRating:nil]; +} + +- (void)presentRating:(NSString *)nameIDorTag { + CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@", nameIDorTag); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag]; +} + + +-(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag { + [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { + if (error) { + CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); + return; + } + + CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); + + NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; + NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; + + CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); + + CountlyFeedbackWidget *widgetToPresent = nil; + + if (nameIDorTag && nameIDorTag.length > 0) { + for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { + if ([nameIDorTag isEqualToString:feedbackWidget.name] || + [nameIDorTag isEqualToString:feedbackWidget.ID] || + [feedbackWidget.tags containsObject:nameIDorTag]) { + widgetToPresent = feedbackWidget; + CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); + break; + } + } + } + + if (!widgetToPresent && filteredWidgets.count > 0) { + widgetToPresent = filteredWidgets.firstObject; + CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); + } + + if (widgetToPresent) { + [widgetToPresent presentWithAppearBlock:^{ + CLY_LOG_D(@"Feedback widget presented. Widget ID: %@, Name: %@", widgetToPresent.ID, widgetToPresent.name); + } andDismissBlock:^{ + CLY_LOG_D(@"Feedback widget dismissed. Widget ID: %@, Name: %@", widgetToPresent.ID, widgetToPresent.name); + }]; + } else { + CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); + } + }]; +} + #endif @end From 520ab8e708be6a6cb44773b596558f710444f676 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 21 Oct 2024 21:32:22 +0500 Subject: [PATCH 099/148] WIP : View tests --- CountlyTests/CountlyBaseTestCase.swift | 2 +- CountlyTests/CountlyViewTests.swift | 524 ++++++++++++++++++++----- CountlyViewTrackingInternal.m | 2 +- 3 files changed, 422 insertions(+), 106 deletions(-) diff --git a/CountlyTests/CountlyBaseTestCase.swift b/CountlyTests/CountlyBaseTestCase.swift index 90a5f60e..d786fa86 100644 --- a/CountlyTests/CountlyBaseTestCase.swift +++ b/CountlyTests/CountlyBaseTestCase.swift @@ -13,7 +13,7 @@ class CountlyBaseTestCase: XCTestCase { var countly: Countly! var deviceID: String = "" let appKey: String = "appkey" - var host: String = "https://test.count.ly/" + var host: String = "https://testing.count.ly/" override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 54f7c1de..38dee23b 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -10,190 +10,506 @@ import XCTest class CountlyViewTrackingTests: CountlyBaseTestCase { - func checkPersistentValues() { - let countlyPersistency = CountlyPersistency.sharedInstance() - if(countlyPersistency != nil) { - if let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? NSMutableArray, - let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? NSMutableArray, - let startedEvents = CountlyPersistency.sharedInstance().value(forKey: "startedEvents") as? NSMutableDictionary, - let isQueueBeingModified = CountlyPersistency.sharedInstance().value(forKey: "isQueueBeingModified") as? Bool { - print("Successfully access private properties.") - - - } - else { - print("Failed to access private properties.") - } + func testStartAndStopView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation.fulfill() } + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") } - func testViewForegroundBackground() { + func testStartAndStopViewWithSegmentation() throws { let config = createBaseConfig() - // No Device ID provided during init Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["key": "value"]) + XCTAssertNotNil(viewID, "View should be started successfully with segmentation.") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { - Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation.fulfill() } - let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") - DispatchQueue.global().asyncAfter(deadline: .now() + 10) { // Delayed by 10 seconds - Countly.sharedInstance().views().resumeView(withID: viewID) - resumeViewExpectation.fulfill() - } + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) + } + + func testStartViewAndStopViewWithID() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - let bgExpectation = XCTestExpectation(description: "Wait for background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 15) { // Delayed by 15 seconds - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation.fulfill() + let viewID = Countly.sharedInstance().views().startView("View1") ?? "" + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withID: viewID) + expectation.fulfill() } - let fgExpectation = XCTestExpectation(description: "Wait for active notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 20) { // Delayed by 20 seconds - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation.fulfill() + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID) + } + + func testStartAndStopMultipleViewsIncludingAutoStoppedViews() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 5 seconds before stopping the views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + expectation.fulfill() } - let bgExpectation1 = XCTestExpectation(description: "Wait for second background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 25) { // Delayed by 25 seconds - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation1.fulfill() + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(withID: viewID2) + } + + func testPauseAndResumeViewsForMultipleViews() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().pauseView(withID: viewID1) + + // Now wait for 3 seconds before resuming the view + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID1) + + // Wait for another 4 seconds before stopping the views + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + expectation.fulfill() + } + } } - let fgExpectation1 = XCTestExpectation(description: "Wait for second active notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 30) { // Delayed by 30 seconds - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation1.fulfill() + wait(for: [expectation], timeout: 15.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(withID: viewID2) + } + + func testMultiplePauseAndResumeCyclesOnSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().pauseView(withID: viewID) + + let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [resumeExpectation], timeout: 13.0) } - // Wait for all expectations or timeout - wait(for: [pauseViewExpectation, resumeViewExpectation, bgExpectation, fgExpectation, bgExpectation1, fgExpectation1], timeout: 35) + wait(for: [expectation], timeout: 10.0) + checkRecordedEventsForView(viewName: "View1") + } + + func testStartViewWhileAutoViewTrackingEnabled() throws { + let config = createBaseConfig() + config.enableAutomaticViewTracking = true + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") + } + + func testStartAndStopAutoStoppedViewWithSegmentation() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("startView") + let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully with segmentation.") - checkPersistentValues() - Countly.sharedInstance().views().stopAllViews(nil); + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withID: viewID) + expectation.fulfill() + } - checkPersistentValues() + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - - func testViewTrackingInit_CNR_AV() throws { + func testStartAutoStoppedViewAndInitiateAnother() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedView") + let viewID1 = Countly.sharedInstance().views().startAutoStoppedView("View1") + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + var viewID2 = "" + let expectation = XCTestExpectation(description: "Wait for 4 seconds before starting the second view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + let stopExpectation = XCTestExpectation(description: "Wait for 3 seconds before stopping both views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withID: viewID1) + Countly.sharedInstance().views().stopView(withID: viewID2) + + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) + checkRecordedEventsForView(withID: viewID1) + checkRecordedEventsForView(withID: viewID2) + } + + func testStartRegularViewPauseAndResumeMultipleTimesThenStop() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) - XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + let expectation = XCTestExpectation(description: "Wait for 3 seconds before pausing the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + + let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().resumeView(withID: viewID) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [resumeExpectation], timeout: 7.0) } - wait(for: [pauseViewExpectation], timeout: 10) + wait(for: [expectation], timeout: 10.0) + checkRecordedEventsForView(viewName: "View1") } + - func testViewTrackingInit_CR_CNG_AV() throws { + func testStopAllViewsWithSpecificSegmentation() throws { let config = createBaseConfig() - config.requiresConsent = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithoutConsent") - XCTAssertNil(viewID, "Auto-stopped view should not be started when consent is not given.") + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().startView("View2") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopAllViews(["key": "value"]) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + checkAllViewsStoppedWithSegmentation(["key": "value"]) } - func testViewTrackingInit_CR_CGV_AV() throws { + func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { let config = createBaseConfig() - config.requiresConsent = true - config.consents = [CLYConsent.viewTracking] Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestAutoStoppedViewWithConsent") - XCTAssertNotNil(viewID, "Auto-stopped view should be started when view tracking consent is given.") + Countly.sharedInstance().views().startView("View1") + + let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } - func testManualViewTrackingInit_CNR_MV() throws { + func testAddSegmentationToAlreadyStartedViewUsingViewID() throws { let config = createBaseConfig() - config.manualSessionHandling = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualView") - XCTAssertNotNil(viewID, "Manual view should be started successfully.") + let viewID = Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().stopView(withID: viewID) + let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withID: viewID) + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + + wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - func testManualViewTrackingInit_CR_CNG_MV() throws { + func testStartViewWithConsentNotGiven() throws { let config = createBaseConfig() - config.manualSessionHandling = true config.requiresConsent = true Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualViewWithoutConsent") - XCTAssertNil(viewID, "Manual view should not be started when consent is not given.") + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNil(viewID, "Event should not be recorded when consent is not given.") + + Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events + checkNoRecordedEvents() } - func testManualViewTrackingInit_CR_CGV_MV() throws { + func testSetAndUpdateGlobalViewSegmentationWithViewInteractions() throws { let config = createBaseConfig() - config.manualSessionHandling = true - config.requiresConsent = true - config.consents = [CLYConsent.viewTracking] Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestManualViewWithConsent") - XCTAssertNotNil(viewID, "Manual view should be started when view tracking consent is given.") + Countly.sharedInstance().views().startView("View1") + + let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View1.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View1") + + Countly.sharedInstance().views().setGlobalViewSegmentation(["key": "value"]) + + let startExpectation = XCTestExpectation(description: "Wait for 3 seconds before starting View2.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().startView("View2") + Countly.sharedInstance().views().updateGlobalViewSegmentation(["key": "newValue"]) + + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View2.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().stopView(withName: "View2") + stopExpectation.fulfill() + } + self.wait(for: [stopExpectation], timeout: 5.0) + } + self.wait(for: [startExpectation], timeout: 7.0) + } - Countly.sharedInstance().views().stopView(withID: viewID) + wait(for: [expectation], timeout: 10.0) // Wait for the expectation to be fulfilled + checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } - func testPauseAndResumeViewTracking() throws { + func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("TestViewPauseResume") - XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully.") + Countly.sharedInstance().views().startView("View1") - let pauseViewExpectation = XCTestExpectation(description: "Wait for pause view") - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { - Countly.sharedInstance().views().pauseView(withID: viewID) - pauseViewExpectation.fulfill() + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - let resumeViewExpectation = XCTestExpectation(description: "Wait for resume view") - DispatchQueue.global().asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().resumeView(withID: viewID) - resumeViewExpectation.fulfill() + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + } + + func testStartMultipleViewsMoveAppToBackgroundAndReturnToForeground() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().startAutoStoppedView("View2") + + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withName: "View2") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [pauseViewExpectation, resumeViewExpectation], timeout: 10) + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + checkRecordedEventsForView(viewName: "View2") } - func testViewTrackingWithBackgroundAndForegroundNotifications() throws { + func testStartViewBackgroundAppResumeViewWhenReturningToForeground() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("TestViewNotifications") + Countly.sharedInstance().views().startView("View1") - let bgExpectation = XCTestExpectation(description: "Wait for background notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - bgExpectation.fulfill() + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - let fgExpectation = XCTestExpectation(description: "Wait for foreground notification") - DispatchQueue.global().asyncAfter(deadline: .now() + 10) { - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - fgExpectation.fulfill() + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1") + } + + func testAttemptToStopANonStartedView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + // Attempt to stop a non-started view +// let beforeEventCount = getRecordedEventCount() + Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") +// let afterEventCount = getRecordedEventCount() + +// XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + } + + func testUpdateSegmentationMultipleTimesOnTheSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + + let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + + let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForStop.fulfill() + } + self.wait(for: [waitForStop], timeout: 5.0) + } + self.wait(for: [waitForSecondSegmentation], timeout: 5.0) + } + + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + } + + func testBackgroundAndForegroundTriggers() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + Countly.sharedInstance().views().startView("View1") + + let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Simulating app going to background + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + // Simulating app returning to foreground + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + waitForForeground.fulfill() + } + self.wait(for: [waitForForeground], timeout: 5.0) + } + self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [bgExpectation, fgExpectation], timeout: 15) - XCTAssertNotNil(viewID, "View should handle background and foreground notifications correctly.") + wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + } + + + // Helper methods to validate results + private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { + // Implement your logic to check recorded events for the specified view + } + + private func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { + // Implement your logic to check recorded events for the specified view ID + } + + private func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { + // Implement your logic to check that all views have been stopped with specific segmentation + } + + private func checkGlobalSegmentationApplied(expected: [String: String]) { + // Implement your logic to verify global segmentation applied correctly + } + + private func checkNoRecordedEvents() { + // Implement logic to verify no recorded events } } diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 89af7504..de91ad17 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -10,7 +10,7 @@ @interface CountlyViewTrackingInternal () #if (TARGET_OS_IOS || TARGET_OS_TV) @property (nonatomic) NSMutableSet* automaticViewTrackingExclusionList; #endif -@property (nonatomic) NSMutableDictionary * viewDataDictionary; +@property (nonatomic, strong) NSMutableDictionary * viewDataDictionary; @property (nonatomic) NSMutableDictionary* viewSegmentation; @property (nonatomic) BOOL isFirstView; @end From b459fe3459379cace0b0946ade54c6bb15b5dea0 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 21 Oct 2024 21:36:56 +0500 Subject: [PATCH 100/148] Fixed feedback widget url encoding issue for custom string --- CountlyFeedbackWidget.m | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CountlyFeedbackWidget.m b/CountlyFeedbackWidget.m index f93863e0..560ac471 100644 --- a/CountlyFeedbackWidget.m +++ b/CountlyFeedbackWidget.m @@ -199,8 +199,7 @@ - (NSURLRequest *)displayRequest // customParams is an NSDictionary containing the custom key-value pairs NSDictionary *customParams = @{@"tc": @"1"}; - // Build custom parameter string - NSMutableString *customString = [NSMutableString stringWithString:@"&custom="]; + NSMutableString *customString = [NSMutableString new]; [customString appendString:@"{"]; [customParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { [customString appendFormat:@"\"%@\":%@,", key, obj]; @@ -208,8 +207,8 @@ - (NSURLRequest *)displayRequest [customString deleteCharactersInRange:NSMakeRange(customString.length - 1, 1)]; // Remove the last comma [customString appendString:@"}"]; - // Append custom parameter - [URL appendString:customString]; + // Append the custom parameter to the URL + [URL appendFormat:@"&custom=%@", customString.cly_URLEscaped]; NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; return request; From 952a6f8e9108df90350eaf44c148c3cd8c1838c6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 13:29:34 +0500 Subject: [PATCH 101/148] Optimize display request method --- CountlyFeedbackWidget.m | 73 +++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/CountlyFeedbackWidget.m b/CountlyFeedbackWidget.m index 560ac471..e1af46e1 100644 --- a/CountlyFeedbackWidget.m +++ b/CountlyFeedbackWidget.m @@ -174,46 +174,61 @@ - (NSURLRequest *)dataRequest } } -- (NSURLRequest *)displayRequest -{ - NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", - kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, - kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, - kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion, - kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion, - kCountlyFBKeyPlatform, CountlyDeviceInfo.osName, - kCountlyFBKeyWidgetID, self.ID]; +- (NSURLRequest *)displayRequest { + // Create the base URL with endpoint and feedback type + NSMutableString *URL = [NSMutableString stringWithFormat:@"%@%@/%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointFeedback, + self.type]; - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + // Create a dictionary for query parameters + NSDictionary *queryParams = @{ + kCountlyQSKeyAppKey: CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, + kCountlyQSKeyDeviceID: CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName: CountlyCommon.sharedInstance.SDKName, + kCountlyQSKeySDKVersion: CountlyCommon.sharedInstance.SDKVersion, + kCountlyFBKeyAppVersion: CountlyDeviceInfo.appVersion, + kCountlyFBKeyPlatform: CountlyDeviceInfo.osName, + kCountlyFBKeyWidgetID: self.ID, + kCountlyAppVersionKey: CountlyDeviceInfo.appVersion, + }; + // Create the query string + NSMutableArray *queryItems = [NSMutableArray array]; + [queryParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [queryItems addObject:[NSString stringWithFormat:@"%@=%@", key, obj]]; + }]; + + NSString *queryString = [queryItems componentsJoinedByString:@"&"]; + + // Append checksum to the query string queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointFeedback]; - NSString* feedbackTypeEndpoint = [@"/" stringByAppendingString:self.type]; - [URL appendString:feedbackTypeEndpoint]; + // Add the query string to the URL [URL appendFormat:@"?%@", queryString]; - // customParams is an NSDictionary containing the custom key-value pairs - NSDictionary *customParams = @{@"tc": @"1"}; + // Create custom parameters + NSDictionary *customParams = @{@"tc": @"1", + @"rw": @"1", + @"xb": @"1"}; - NSMutableString *customString = [NSMutableString new]; - [customString appendString:@"{"]; - [customParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - [customString appendFormat:@"\"%@\":%@,", key, obj]; - }]; - [customString deleteCharactersInRange:NSMakeRange(customString.length - 1, 1)]; // Remove the last comma - [customString appendString:@"}"]; + // Create JSON data from custom parameters + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:customParams options:0 error:&error]; - // Append the custom parameter to the URL - [URL appendFormat:@"&custom=%@", customString.cly_URLEscaped]; + if (!jsonData) { + NSLog(@"Failed to serialize JSON: %@", error); + } else { + NSString *customString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + // Append the custom parameter to the URL + [URL appendFormat:@"&custom=%@", customString.cly_URLEscaped]; + } - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; + // Create and return the NSURLRequest + return [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; } + - (void)recordReservedEventForDismissing { [self recordReservedEventWithSegmentation:@{kCountlyFBKeyClosed: @1}]; From 8a624ef7b9b8847826d0f8a7d54382d6a754cb5a Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 13:34:55 +0500 Subject: [PATCH 102/148] Added changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f56a0e..1505dc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 24.7.4 +* Mitigated an issue with the feedback widget that caused it to display a blank white page on devices running iOS versions earlier than 17 + ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) * Updated the SDK to ensure compatibility with the latest server response models From cfa4d4174419f13bca06a96fcd4d3a0dd3bc0c09 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 13:39:56 +0500 Subject: [PATCH 103/148] Removed extra custom params --- CountlyFeedbackWidget.m | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CountlyFeedbackWidget.m b/CountlyFeedbackWidget.m index e1af46e1..eeeba2d6 100644 --- a/CountlyFeedbackWidget.m +++ b/CountlyFeedbackWidget.m @@ -208,9 +208,7 @@ - (NSURLRequest *)displayRequest { [URL appendFormat:@"?%@", queryString]; // Create custom parameters - NSDictionary *customParams = @{@"tc": @"1", - @"rw": @"1", - @"xb": @"1"}; + NSDictionary *customParams = @{@"tc": @"1"}; // Create JSON data from custom parameters NSError *error; From d971ef484df378c1074aadb2de6c61d8a6b3ad2f Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 19:06:39 +0500 Subject: [PATCH 104/148] Added new interface for feedbacks and implemented callbacks for new feedback methods --- Countly-PL.podspec | 2 +- Countly.h | 9 +- Countly.m | 18 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 24 +- CountlyCommon.h | 2 +- CountlyConfig.h | 8 + CountlyConsentManager.m | 2 +- CountlyFeedbackWidget.h | 7 + CountlyFeedbackWidget.m | 25 +- CountlyFeedbacks.h | 32 +- CountlyFeedbacks.m | 564 ++--------------------------- CountlyFeedbacksInternal.h | 41 +++ CountlyFeedbacksInternal.m | 568 ++++++++++++++++++++++++++++++ 14 files changed, 720 insertions(+), 584 deletions(-) create mode 100644 CountlyFeedbacksInternal.h create mode 100644 CountlyFeedbacksInternal.m diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 37cf21a3..c7186a68 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h", "CountlyFeedbacks.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/Countly.h b/Countly.h index 7fe85635..a4a337fd 100644 --- a/Countly.h +++ b/Countly.h @@ -13,6 +13,7 @@ #import "CountlyFeedbackWidget.h" #import "CountlyViewTracking.h" #import "CountlyContentBuilder.h" +#import "CountlyFeedbacks.h" #import "Resettable.h" #if (TARGET_OS_IOS || TARGET_OS_OSX) #import @@ -676,8 +677,7 @@ NS_ASSUME_NONNULL_BEGIN * @discussion - Current device ID is @c CLYTemporaryDeviceID. * @param completionHandler A completion handler block to be executed when list is fetched successfully or there is an error. */ -- (void)getFeedbackWidgets:(void (^)(NSArray * __nullable feedbackWidgets, NSError * __nullable error))completionHandler; - +- (void)getFeedbackWidgets:(void (^)(NSArray * __nullable feedbackWidgets, NSError * __nullable error))completionHandler DEPRECATED_MSG_ATTRIBUTE("Use '[feedback getAvailableFeedbackWidgets:]' method instead!"); /** * This is an experimental feature and it can have breaking changes @@ -686,6 +686,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (CountlyContentBuilder *_Nonnull) content; +/** + * Interface variable to access feedback widget functionalities. + * @discussion Feedback widget interface for developer to interact with SDK. + */ +- (CountlyFeedbacks *) feedback; #endif diff --git a/Countly.m b/Countly.m index d6e46173..be006661 100644 --- a/Countly.m +++ b/Countly.m @@ -175,11 +175,11 @@ - (void)startWithConfig:(CountlyConfig *)config } #if (TARGET_OS_IOS) - CountlyFeedbacks.sharedInstance.message = config.starRatingMessage; - CountlyFeedbacks.sharedInstance.sessionCount = config.starRatingSessionCount; - CountlyFeedbacks.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; - CountlyFeedbacks.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; - [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; + CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; + CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; + CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; + CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; #endif if(config.disableLocation) @@ -1240,7 +1240,7 @@ - (void)askForStarRating:(void(^)(NSInteger rating))completion { CLY_LOG_I(@"%s %@", __FUNCTION__, completion); - [CountlyFeedbacks.sharedInstance showDialog:completion]; + [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; } - (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler @@ -1262,21 +1262,21 @@ - (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString CLY_LOG_I(@"%s %@ %@ %@", __FUNCTION__, widgetID, closeButtonText, completionHandler); - [CountlyFeedbacks.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; } - (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted { CLY_LOG_I(@"%s %@ %ld %@ %@ %d", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); - [CountlyFeedbacks.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; + [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; } - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; } - (CountlyContentBuilder *) content diff --git a/Countly.podspec b/Countly.podspec index 17ed0ab4..04cc60a0 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -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', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h" + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h", "CountlyFeedbacks.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] end diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 1394aab3..d6bc336f 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */; }; 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ED72BEBD22100139F91 /* CountlyCrashData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399B464F2C52813700AD384E /* CountlyLocationTests.swift */; }; + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */; }; 39EE1F102C8B341E0016D1BF /* CountlyExperimentalConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9872245225A00E3D7AE /* Countly.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9852245225A00E3D7AE /* Countly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A98D2245228300E3D7AE /* CountlyConnectionManager.h */; }; @@ -85,8 +87,8 @@ D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */ = {isa = PBXBuildFile; fileRef = D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */; }; D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; D249BF5F254D3D180058A6C2 /* CountlyFeedbackWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */; }; - D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */; }; - D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */; }; + D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */; }; + D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -143,6 +145,8 @@ 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashData.m; sourceTree = ""; }; 39924ED72BEBD22100139F91 /* CountlyCrashData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyCrashData.h; sourceTree = ""; }; 399B464F2C52813700AD384E /* CountlyLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyLocationTests.swift; sourceTree = ""; }; + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; 3B20A9822245225A00E3D7AE /* Countly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Countly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B20A9852245225A00E3D7AE /* Countly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Countly.h; sourceTree = ""; }; 3B20A9862245225A00E3D7AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -181,8 +185,8 @@ D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPerformanceMonitoring.m; sourceTree = ""; }; D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbackWidget.h; sourceTree = ""; }; D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbackWidget.m; sourceTree = ""; }; - D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; - D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; + D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacksInternal.h; sourceTree = ""; }; + D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacksInternal.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -224,6 +228,8 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */, + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */, 39002D092C8B2E450049394F /* CountlyContentConfig.h */, 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */, 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */, @@ -274,8 +280,8 @@ 3B20A9AA2245228500E3D7AE /* CountlyDeviceInfo.m */, 3B20A99E2245228400E3D7AE /* CountlyEvent.h */, 3B20A9952245228400E3D7AE /* CountlyEvent.m */, - D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */, - D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */, + D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */, + D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */, D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */, D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */, 3B20A99A2245228400E3D7AE /* CountlyLocationManager.h */, @@ -326,6 +332,7 @@ files = ( 39924ECE2BEBD0B700139F91 /* CountlyCrashesConfig.h in Headers */, 1A478D032AB314750056A5E7 /* CountlyExperimentInformation.h in Headers */, + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */, 3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */, 3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */, 3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */, @@ -357,7 +364,7 @@ D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */, 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */, 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */, - D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */, + D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */, 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -489,10 +496,11 @@ 3B20A9D42245228700E3D7AE /* CountlyPersistency.m in Sources */, 3B20A9B32245228700E3D7AE /* CountlyNotificationService.m in Sources */, 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */, - D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */, + D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */, 1A3110632A7128CD001CB507 /* CountlyViewData.m in Sources */, 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */, 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */, + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */, 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, diff --git a/CountlyCommon.h b/CountlyCommon.h index 3a1f4564..69a29fce 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -15,7 +15,7 @@ #import "CountlyCrashReporter.h" #import "CountlyConfig.h" #import "CountlyViewTrackingInternal.h" -#import "CountlyFeedbacks.h" +#import "CountlyFeedbacksInternal.h" #import "CountlyFeedbackWidget.h" #import "CountlyPushNotifications.h" #import "CountlyNotificationService.h" diff --git a/CountlyConfig.h b/CountlyConfig.h index dfc0397f..411733fd 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -20,6 +20,14 @@ NS_ASSUME_NONNULL_BEGIN +typedef enum : NSUInteger +{ + WIDGET_APPEARED, + WIDGET_CLOSED, +} WidgetState; + +typedef void (^WidgetCallback)(WidgetState widgetState); + //NOTE: Countly features typedef NSString* CLYFeature NS_EXTENSIBLE_STRING_ENUM; #if (TARGET_OS_IOS) diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index fc4bf12f..57f182b0 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -438,7 +438,7 @@ - (void)setConsentForFeedback:(BOOL)consentForFeedback { CLY_LOG_D(@"Consent for Feedback is given."); - [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; } else { diff --git a/CountlyFeedbackWidget.h b/CountlyFeedbackWidget.h index 73a91582..9bce328b 100644 --- a/CountlyFeedbackWidget.h +++ b/CountlyFeedbackWidget.h @@ -5,6 +5,7 @@ // Please visit www.count.ly for more information. #import +#import "CountlyConfig.h" NS_ASSUME_NONNULL_BEGIN @@ -41,6 +42,12 @@ extern NSString* const kCountlyReservedEventRating; */ - (void)presentWithAppearBlock:(void(^ __nullable)(void))appearBlock andDismissBlock:(void(^ __nullable)(void))dismissBlock; +/** + * Modally presents the feedback widget above the top visible view controller and executes given blocks. + * @discussion Calls to this method will be ignored if consent for @c CLYConsentFeedback is not given while @c requiresConsent flag is set on initial configuration. + * @param wigetCallback Block to be executed when widget is displayed/dismissed + */ +- (void)presentWithCallback:(WidgetCallback) wigetCallback; /** * Fetches feedback widget's data to be used for manually presenting it. * @discussion When feedback widget's data is fetched successfully, @c completionHandler will be executed with an @c NSDictionary diff --git a/CountlyFeedbackWidget.m b/CountlyFeedbackWidget.m index f93863e0..a5438bf1 100644 --- a/CountlyFeedbackWidget.m +++ b/CountlyFeedbackWidget.m @@ -52,6 +52,20 @@ - (void)present - (void)presentWithAppearBlock:(void(^ __nullable)(void))appearBlock andDismissBlock:(void(^ __nullable)(void))dismissBlock { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, appearBlock, dismissBlock); + [self presentWithCallback:^(WidgetState widgetState) { + if(appearBlock && widgetState == WIDGET_APPEARED) { + appearBlock(); + } + + if(dismissBlock && widgetState == WIDGET_CLOSED) { + dismissBlock(); + } + }]; +} + +- (void)presentWithCallback:(WidgetCallback) widgetCallback; +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, widgetCallback); if (!CountlyConsentManager.sharedInstance.consentForFeedback) return; __block CLYInternalViewController* webVC = CLYInternalViewController.new; @@ -75,15 +89,20 @@ - (void)presentWithAppearBlock:(void(^ __nullable)(void))appearBlock andDismissB { [webVC dismissViewControllerAnimated:YES completion:^ { - if (dismissBlock) - dismissBlock(); + CLY_LOG_D(@"Feedback widget dismissed. Widget ID: %@, Name: %@", self.ID, self.name); + if (widgetCallback) + widgetCallback(WIDGET_CLOSED); webVC = nil; }]; [self recordReservedEventForDismissing]; }; [webView addSubview:dismissButton]; [dismissButton positionToTopRight]; - [CountlyCommon.sharedInstance tryPresentingViewController:webVC withCompletion:appearBlock]; + [CountlyCommon.sharedInstance tryPresentingViewController:webVC withCompletion:^{ + CLY_LOG_D(@"Feedback widget presented. Widget ID: %@, Name: %@", self.ID, self.name); + if(widgetCallback) + widgetCallback(WIDGET_APPEARED); + }]; } - (void)getWidgetData:(void (^)(NSDictionary * __nullable widgetData, NSError * __nullable error))completionHandler diff --git a/CountlyFeedbacks.h b/CountlyFeedbacks.h index 48d2aeab..446d1e55 100644 --- a/CountlyFeedbacks.h +++ b/CountlyFeedbacks.h @@ -1,44 +1,30 @@ -// CountlyFeedbacks.h +// CountlyFeedbacks.h // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. +// #import +#import "CountlyConfig.h" +#import "CountlyFeedbackWidget.h" -@class CountlyFeedbackWidget; - -extern NSString* const kCountlyFBKeyPlatform; -extern NSString* const kCountlyFBKeyAppVersion; -extern NSString* const kCountlyFBKeyWidgetID; -extern NSString* const kCountlyFBKeyID; - -extern NSString* const kCountlyReservedEventStarRating; - -@interface CountlyFeedbacks : NSObject +@interface CountlyFeedbacks: NSObject #if (TARGET_OS_IOS) + (instancetype)sharedInstance; -- (void)showDialog:(void(^)(NSInteger rating))completion; -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; -- (void)checkForStarRatingAutoAsk; - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; - - (void) presentNPS; - (void) presentNPS:(NSString *)nameIDorTag; +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; - (void) presentSurvey; - (void) presentSurvey:(NSString *)nameIDorTag; +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; - (void) presentRating; - (void) presentRating:(NSString *)nameIDorTag; +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; -@property (nonatomic) NSString* message; -@property (nonatomic) NSString* dismissButtonTitle; -@property (nonatomic) NSUInteger sessionCount; -@property (nonatomic) BOOL disableAskingForEachAppVersion; -@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler; #endif @end diff --git a/CountlyFeedbacks.m b/CountlyFeedbacks.m index 25fb4bcb..5b566c0d 100644 --- a/CountlyFeedbacks.m +++ b/CountlyFeedbacks.m @@ -1,57 +1,20 @@ -// CountlyFeedbacks.m +// CountlyFeedbacks.m // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. +// +#import "CountlyFeedbacks.h" #import "CountlyCommon.h" -#if (TARGET_OS_IOS) -#import -#endif - -@interface CountlyFeedbackWidget () -+ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; -@end - - - -@interface CountlyFeedbacks () -#if (TARGET_OS_IOS) -@property (nonatomic) UIAlertController* alertController; -@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); -#endif -@end - -NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; -NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; -NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; - -NSString* const kCountlyFBKeyPlatform = @"platform"; -NSString* const kCountlyFBKeyAppVersion = @"app_version"; -NSString* const kCountlyFBKeyRating = @"rating"; -NSString* const kCountlyFBKeyWidgetID = @"widget_id"; -NSString* const kCountlyFBKeyID = @"_id"; -NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; -NSString* const kCountlyFBKeyPhone = @"phone"; -NSString* const kCountlyFBKeyTablet = @"tablet"; -NSString* const kCountlyFBKeyFeedback = @"feedback"; -NSString* const kCountlyFBKeyEmail = @"email"; -NSString* const kCountlyFBKeyComment = @"comment"; -NSString* const kCountlyFBKeyContactMe = @"contactMe"; - -const CGFloat kCountlyStarRatingButtonSize = 40.0; @implementation CountlyFeedbacks #if (TARGET_OS_IOS) -{ - UIButton* btn_star[5]; -} - + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) return nil; - + static CountlyFeedbacks* s_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); @@ -60,525 +23,56 @@ + (instancetype)sharedInstance - (instancetype)init { - if (self = [super init]) - { - NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; - - NSDictionary* dictMessage = - @{ - @"en": @"How would you rate the app?", - @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", - @"ja": @"あなたの評価を教えてください。", - @"zh": @"请告诉我你的评价。", - @"ru": @"Как бы вы оценили приложение?", - @"cz": @"Jak hodnotíte aplikaci?", - @"lv": @"Kā Jūs novērtētu šo lietotni?", - @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", - @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", - }; - - self.message = dictMessage[langDesignator]; - if (!self.message) - self.message = dictMessage[@"en"]; - } - - return self; -} - -#pragma mark - Star Rating - -- (void)showDialog:(void(^)(NSInteger rating))completion -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - self.ratingCompletion = completion; - - self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton]; - dismissButton.onClick = ^(id sender) - { - [self.alertController dismissViewControllerAnimated:YES completion:^ - { - [self finishWithRating:0]; - }]; - }; - [self.alertController.view addSubview:dismissButton]; - [dismissButton positionToTopRight]; - - CLYInternalViewController* cvc = CLYInternalViewController.new; - [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; - [cvc.view addSubview:[self starView]]; - - @try - { - [self.alertController setValue:cvc forKey:@"contentViewController"]; - } - @catch (NSException* exception) - { - CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); - } - - [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; -} - -- (void)checkForStarRatingAutoAsk -{ - if (!self.sessionCount) - return; - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; - - if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) - return; - - NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; - NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; - sessionCountSoFar++; - - if (self.sessionCount == sessionCountSoFar) - { - CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); - - [self showDialog:self.ratingCompletionForAutoAsk]; - - status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; - } - - status[keyForAppVersion] = @(sessionCountSoFar); - - [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; -} - -- (UIView *)starView -{ - UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; - vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; - - for (int i = 0; i < 5; i++) - { - btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; - btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; - [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; - [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; - [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; - - [vw_star addSubview:btn_star[i]]; - } - - return vw_star; -} - -- (void)setMessage:(NSString *)message -{ - if (!message) - return; - - _message = message; -} - -- (void)onClick_star:(id)sender -{ - UIColor* color = [self activeStarColor]; - NSInteger rating = 0; - - for (int i = 0; i < 5; i++) - { - [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; - - if (btn_star[i] == sender) - { - color = [self passiveStarColor]; - rating = i + 1; - } - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ - { - [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; - }); -} - -- (void)finishWithRating:(NSInteger)rating -{ - if (self.ratingCompletion) - self.ratingCompletion(rating); - - if (rating != 0) - { - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; - } - - self.alertController = nil; - self.ratingCompletion = nil; -} - -- (UIColor *)activeStarColor -{ - return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; -} - -- (UIColor *)passiveStarColor -{ - return [UIColor colorWithWhite:178/255.0 alpha:1]; -} - -#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) - -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); - return; - } + self = [super init]; - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - if (!widgetID.length) - return; - - NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary* widgetInfo = nil; - - if (!error) - { - widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - NSMutableDictionary* userInfo = widgetInfo.mutableCopy; - - if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; - } - else if (![self isDeviceTargetedByWidget:widgetInfo]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(error); - }); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; - }); - }]; - - [task resume]; -} - -- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler -{ - __block CLYInternalViewController* webVC = CLYInternalViewController.new; - webVC.view.backgroundColor = UIColor.whiteColor; - webVC.view.bounds = UIScreen.mainScreen.bounds; - webVC.modalPresentationStyle = UIModalPresentationCustom; - - WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; - webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [webVC.view addSubview:webView]; - NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; - [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; - dismissButton.onClick = ^(id sender) - { - [webVC dismissViewControllerAnimated:YES completion:^ - { - if (completionHandler) - completionHandler(nil); - - webVC = nil; - }]; - }; - [webVC.view addSubview:dismissButton]; - [dismissButton positionToTopRightConsideringStatusBar]; - - [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; -} - -- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", - kCountlyEndpointO, - kCountlyEndpointFeedback, - kCountlyEndpointWidget]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; - return request; - } -} - -- (NSURL *)widgetDisplayURL:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", - kCountlyFBKeyWidgetID, widgetID, - kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", - CountlyConnectionManager.sharedInstance.host, - kCountlyEndpointFeedback, - queryString]; - - return [NSURL URLWithString:URLString]; -} - -- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo -{ - BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; - BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; - BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; - BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; - - return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); -} - -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (!widgetID.length) - return; - - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - segmentation[kCountlyFBKeyWidgetID] = widgetID; - segmentation[kCountlyFBKeyEmail] = email; - segmentation[kCountlyFBKeyComment] = comment; - segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; -} - - -#pragma mark - Feedbacks (Surveys, NPS) - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary *feedbacksResponse = nil; - - if (!error) - { - feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - if (((NSHTTPURLResponse*)response).statusCode != 200) - { - NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; - userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(nil, error); - }); - - return; - } - - NSMutableArray* feedbacks = NSMutableArray.new; - NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; - for (NSDictionary * feedbackDict in rawFeedbackObjects) - { - CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; - if (feedback) - [feedbacks addObject:feedback]; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler([NSArray arrayWithArray:feedbacks], nil); - }); - }]; - - [task resume]; + return self; } -- (NSURLRequest *)feedbacksRequest +- (void)enterContentZone:(NSArray *)tags { - NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", - kCountlyQSKeyMethod, kCountlyFBKeyFeedback, - kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, - kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, - kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointO]; - [URL appendString:kCountlyEndpointSDK]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - [URL appendFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; - } + [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; } - (void)presentNPS { - [self presentNPS:nil]; + [self presentNPS:nil widgetCallback:nil]; } - (void)presentNPS:(NSString *)nameIDorTag { - CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@", nameIDorTag); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag]; + [self presentNPS:nameIDorTag widgetCallback:nil]; +} + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; } - (void)presentSurvey { - [self presentSurvey:nil]; + [self presentSurvey:nil widgetCallback:nil]; } - (void)presentSurvey:(NSString *)nameIDorTag { - CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@", nameIDorTag); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag]; + [self presentSurvey:nameIDorTag widgetCallback:nil]; +} + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; } - (void)presentRating { - [self presentRating:nil]; + [self presentRating:nil widgetCallback:nil]; } - (void)presentRating:(NSString *)nameIDorTag { - CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@", nameIDorTag); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag]; + [self presentRating:nameIDorTag widgetCallback:nil]; } - --(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag { - [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { - if (error) { - CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); - return; - } - - CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); - - NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; - NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; - - CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); - - CountlyFeedbackWidget *widgetToPresent = nil; - - if (nameIDorTag && nameIDorTag.length > 0) { - for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { - if ([nameIDorTag isEqualToString:feedbackWidget.name] || - [nameIDorTag isEqualToString:feedbackWidget.ID] || - [feedbackWidget.tags containsObject:nameIDorTag]) { - widgetToPresent = feedbackWidget; - CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); - break; - } - } - } - - if (!widgetToPresent && filteredWidgets.count > 0) { - widgetToPresent = filteredWidgets.firstObject; - CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); - } - - if (widgetToPresent) { - [widgetToPresent presentWithAppearBlock:^{ - CLY_LOG_D(@"Feedback widget presented. Widget ID: %@, Name: %@", widgetToPresent.ID, widgetToPresent.name); - } andDismissBlock:^{ - CLY_LOG_D(@"Feedback widget dismissed. Widget ID: %@, Name: %@", widgetToPresent.ID, widgetToPresent.name); - }]; - } else { - CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); - } - }]; +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; } +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; +} #endif @end diff --git a/CountlyFeedbacksInternal.h b/CountlyFeedbacksInternal.h new file mode 100644 index 00000000..85518ebe --- /dev/null +++ b/CountlyFeedbacksInternal.h @@ -0,0 +1,41 @@ +// CountlyFeedbacksInternal.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +@class CountlyFeedbackWidget; + +extern NSString* const kCountlyFBKeyPlatform; +extern NSString* const kCountlyFBKeyAppVersion; +extern NSString* const kCountlyFBKeyWidgetID; +extern NSString* const kCountlyFBKeyID; + +extern NSString* const kCountlyReservedEventStarRating; + +@interface CountlyFeedbacksInternal : NSObject +#if (TARGET_OS_IOS) ++ (instancetype)sharedInstance; + +- (void)showDialog:(void(^)(NSInteger rating))completion; +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; +- (void)checkForStarRatingAutoAsk; + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +@property (nonatomic) NSString* message; +@property (nonatomic) NSString* dismissButtonTitle; +@property (nonatomic) NSUInteger sessionCount; +@property (nonatomic) BOOL disableAskingForEachAppVersion; +@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); +#endif +@end diff --git a/CountlyFeedbacksInternal.m b/CountlyFeedbacksInternal.m new file mode 100644 index 00000000..4a7bb9e2 --- /dev/null +++ b/CountlyFeedbacksInternal.m @@ -0,0 +1,568 @@ +// CountlyFeedbacks.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyCommon.h" +#if (TARGET_OS_IOS) +#import +#endif + +@interface CountlyFeedbackWidget () ++ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; +@end + + + +@interface CountlyFeedbacksInternal () +#if (TARGET_OS_IOS) +@property (nonatomic) UIAlertController* alertController; +@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); +#endif +@end + +NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; +NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; +NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; + +NSString* const kCountlyFBKeyPlatform = @"platform"; +NSString* const kCountlyFBKeyAppVersion = @"app_version"; +NSString* const kCountlyFBKeyRating = @"rating"; +NSString* const kCountlyFBKeyWidgetID = @"widget_id"; +NSString* const kCountlyFBKeyID = @"_id"; +NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; +NSString* const kCountlyFBKeyPhone = @"phone"; +NSString* const kCountlyFBKeyTablet = @"tablet"; +NSString* const kCountlyFBKeyFeedback = @"feedback"; +NSString* const kCountlyFBKeyEmail = @"email"; +NSString* const kCountlyFBKeyComment = @"comment"; +NSString* const kCountlyFBKeyContactMe = @"contactMe"; + +const CGFloat kCountlyStarRatingButtonSize = 40.0; + +@implementation CountlyFeedbacksInternal +#if (TARGET_OS_IOS) +{ + UIButton* btn_star[5]; +} + ++ (instancetype)sharedInstance +{ + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + + static CountlyFeedbacksInternal* s_sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; +} + +- (instancetype)init +{ + if (self = [super init]) + { + NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; + + NSDictionary* dictMessage = + @{ + @"en": @"How would you rate the app?", + @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", + @"ja": @"あなたの評価を教えてください。", + @"zh": @"请告诉我你的评价。", + @"ru": @"Как бы вы оценили приложение?", + @"cz": @"Jak hodnotíte aplikaci?", + @"lv": @"Kā Jūs novērtētu šo lietotni?", + @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", + @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", + }; + + self.message = dictMessage[langDesignator]; + if (!self.message) + self.message = dictMessage[@"en"]; + } + + return self; +} + +#pragma mark - Star Rating + +- (void)showDialog:(void(^)(NSInteger rating))completion +{ + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + self.ratingCompletion = completion; + + self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton]; + dismissButton.onClick = ^(id sender) + { + [self.alertController dismissViewControllerAnimated:YES completion:^ + { + [self finishWithRating:0]; + }]; + }; + [self.alertController.view addSubview:dismissButton]; + [dismissButton positionToTopRight]; + + CLYInternalViewController* cvc = CLYInternalViewController.new; + [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; + [cvc.view addSubview:[self starView]]; + + @try + { + [self.alertController setValue:cvc forKey:@"contentViewController"]; + } + @catch (NSException* exception) + { + CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); + } + + [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; +} + +- (void)checkForStarRatingAutoAsk +{ + if (!self.sessionCount) + return; + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; + + if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) + return; + + NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; + NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; + sessionCountSoFar++; + + if (self.sessionCount == sessionCountSoFar) + { + CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); + + [self showDialog:self.ratingCompletionForAutoAsk]; + + status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; + } + + status[keyForAppVersion] = @(sessionCountSoFar); + + [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; +} + +- (UIView *)starView +{ + UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; + vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + + for (int i = 0; i < 5; i++) + { + btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; + btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; + [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; + [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; + [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; + + [vw_star addSubview:btn_star[i]]; + } + + return vw_star; +} + +- (void)setMessage:(NSString *)message +{ + if (!message) + return; + + _message = message; +} + +- (void)onClick_star:(id)sender +{ + UIColor* color = [self activeStarColor]; + NSInteger rating = 0; + + for (int i = 0; i < 5; i++) + { + [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; + + if (btn_star[i] == sender) + { + color = [self passiveStarColor]; + rating = i + 1; + } + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ + { + [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; + }); +} + +- (void)finishWithRating:(NSInteger)rating +{ + if (self.ratingCompletion) + self.ratingCompletion(rating); + + if (rating != 0) + { + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; + } + + self.alertController = nil; + self.ratingCompletion = nil; +} + +- (UIColor *)activeStarColor +{ + return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; +} + +- (UIColor *)passiveStarColor +{ + return [UIColor colorWithWhite:178/255.0 alpha:1]; +} + +#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) + +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + if (!widgetID.length) + return; + + NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary* widgetInfo = nil; + + if (!error) + { + widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + NSMutableDictionary* userInfo = widgetInfo.mutableCopy; + + if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; + } + else if (![self isDeviceTargetedByWidget:widgetInfo]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(error); + }); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + }); + }]; + + [task resume]; +} + +- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + __block CLYInternalViewController* webVC = CLYInternalViewController.new; + webVC.view.backgroundColor = UIColor.whiteColor; + webVC.view.bounds = UIScreen.mainScreen.bounds; + webVC.modalPresentationStyle = UIModalPresentationCustom; + + WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; + webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [webVC.view addSubview:webView]; + NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; + [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; + dismissButton.onClick = ^(id sender) + { + [webVC dismissViewControllerAnimated:YES completion:^ + { + if (completionHandler) + completionHandler(nil); + + webVC = nil; + }]; + }; + [webVC.view addSubview:dismissButton]; + [dismissButton positionToTopRightConsideringStatusBar]; + + [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; +} + +- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", + kCountlyEndpointO, + kCountlyEndpointFeedback, + kCountlyEndpointWidget]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; + return request; + } +} + +- (NSURL *)widgetDisplayURL:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", + kCountlyFBKeyWidgetID, widgetID, + kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointFeedback, + queryString]; + + return [NSURL URLWithString:URLString]; +} + +- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo +{ + BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; + BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; + BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; + BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; + + return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); +} + +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted +{ + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (!widgetID.length) + return; + + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + segmentation[kCountlyFBKeyWidgetID] = widgetID; + segmentation[kCountlyFBKeyEmail] = email; + segmentation[kCountlyFBKeyComment] = comment; + segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; +} + + +#pragma mark - Feedbacks (Surveys, NPS) + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary *feedbacksResponse = nil; + + if (!error) + { + feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + if (((NSHTTPURLResponse*)response).statusCode != 200) + { + NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; + userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(nil, error); + }); + + return; + } + + NSMutableArray* feedbacks = NSMutableArray.new; + NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; + for (NSDictionary * feedbackDict in rawFeedbackObjects) + { + CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; + if (feedback) + [feedbacks addObject:feedback]; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler([NSArray arrayWithArray:feedbacks], nil); + }); + }]; + + [task resume]; +} + +- (NSURLRequest *)feedbacksRequest +{ + NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", + kCountlyQSKeyMethod, kCountlyFBKeyFeedback, + kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, + kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, + kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; + [URL appendString:kCountlyEndpointO]; + [URL appendString:kCountlyEndpointSDK]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + [URL appendFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; + return request; + } +} + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + + +-(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { + if (error) { + CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); + return; + } + + CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); + + NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; + NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; + + CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); + + CountlyFeedbackWidget *widgetToPresent = nil; + + if (nameIDorTag && nameIDorTag.length > 0) { + for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { + if ([nameIDorTag isEqualToString:feedbackWidget.name] || + [nameIDorTag isEqualToString:feedbackWidget.ID] || + [feedbackWidget.tags containsObject:nameIDorTag]) { + widgetToPresent = feedbackWidget; + CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); + break; + } + } + } + + if (!widgetToPresent && filteredWidgets.count > 0) { + widgetToPresent = filteredWidgets.firstObject; + CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); + } + + if (widgetToPresent) { + [widgetToPresent presentWithCallback:widgetCallback]; + } else { + CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); + } + }]; +} + +#endif +@end From 070cfe8bbe661bbbcd1c4771fb0d658c0b8591c4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 19:16:44 +0500 Subject: [PATCH 105/148] Added changelog entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f56a0e..5403ce31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## x.x.x +* Added `CountlyFeedbacks` interface to manage feedback widgets, including NPS, Surveys, and Ratings. + * `presentNPS`, `presentSurvey`, and `presentRating` methods now allow displaying specific widgets by name, ID, or tag. + * Optional widget callback support for handling custom logic after displaying feedback widgets. + * Added `getAvailableFeedbackWidgets` method to retrieve available feedback widgets with a completion handler. + ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) * Updated the SDK to ensure compatibility with the latest server response models From 2c07598033a8a58c6d65d44e22c9267d448581f8 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 19:42:19 +0500 Subject: [PATCH 106/148] Temp commit to review changes - Renaming files shows lots of changes, due to which its hard to review --- Countly.h | 4 +- Countly.m | 18 +- Countly.xcodeproj/project.pbxproj | 32 +- CountlyCommon.h | 2 +- CountlyConsentManager.m | 2 +- CountlyFeedbacks.h | 41 ++- CountlyFeedbacks.m | 548 ++++++++++++++++++++++++++-- CountlyFeedbacksInternal.h | 41 --- CountlyFeedbacksInternal.m | 568 ------------------------------ CountlyFeedbacksNew.h | 30 ++ CountlyFeedbacksNew.m | 78 ++++ 11 files changed, 682 insertions(+), 682 deletions(-) delete mode 100644 CountlyFeedbacksInternal.h delete mode 100644 CountlyFeedbacksInternal.m create mode 100644 CountlyFeedbacksNew.h create mode 100644 CountlyFeedbacksNew.m diff --git a/Countly.h b/Countly.h index a4a337fd..b0957fec 100644 --- a/Countly.h +++ b/Countly.h @@ -13,7 +13,7 @@ #import "CountlyFeedbackWidget.h" #import "CountlyViewTracking.h" #import "CountlyContentBuilder.h" -#import "CountlyFeedbacks.h" +#import "CountlyFeedbacksNew.h" #import "Resettable.h" #if (TARGET_OS_IOS || TARGET_OS_OSX) #import @@ -690,7 +690,7 @@ NS_ASSUME_NONNULL_BEGIN * Interface variable to access feedback widget functionalities. * @discussion Feedback widget interface for developer to interact with SDK. */ -- (CountlyFeedbacks *) feedback; +- (CountlyFeedbacksNew *) feedback; #endif diff --git a/Countly.m b/Countly.m index be006661..d6e46173 100644 --- a/Countly.m +++ b/Countly.m @@ -175,11 +175,11 @@ - (void)startWithConfig:(CountlyConfig *)config } #if (TARGET_OS_IOS) - CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; - CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; - CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; - CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; - [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; + CountlyFeedbacks.sharedInstance.message = config.starRatingMessage; + CountlyFeedbacks.sharedInstance.sessionCount = config.starRatingSessionCount; + CountlyFeedbacks.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; + CountlyFeedbacks.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; + [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; #endif if(config.disableLocation) @@ -1240,7 +1240,7 @@ - (void)askForStarRating:(void(^)(NSInteger rating))completion { CLY_LOG_I(@"%s %@", __FUNCTION__, completion); - [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; + [CountlyFeedbacks.sharedInstance showDialog:completion]; } - (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler @@ -1262,21 +1262,21 @@ - (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString CLY_LOG_I(@"%s %@ %@ %@", __FUNCTION__, widgetID, closeButtonText, completionHandler); - [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + [CountlyFeedbacks.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; } - (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted { CLY_LOG_I(@"%s %@ %ld %@ %@ %d", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); - [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; + [CountlyFeedbacks.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; } - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; + [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; } - (CountlyContentBuilder *) content diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d6bc336f..ff5a1072 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -48,8 +48,8 @@ 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */; }; 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ED72BEBD22100139F91 /* CountlyCrashData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399B464F2C52813700AD384E /* CountlyLocationTests.swift */; }; - 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */; }; + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacksNew.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacksNew.m in Sources */ = {isa = PBXBuildFile; fileRef = 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */; }; 39EE1F102C8B341E0016D1BF /* CountlyExperimentalConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9872245225A00E3D7AE /* Countly.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9852245225A00E3D7AE /* Countly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A98D2245228300E3D7AE /* CountlyConnectionManager.h */; }; @@ -87,8 +87,8 @@ D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */ = {isa = PBXBuildFile; fileRef = D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */; }; D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; D249BF5F254D3D180058A6C2 /* CountlyFeedbackWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */; }; - D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */; }; - D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */; }; + D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */; }; + D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -145,8 +145,8 @@ 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashData.m; sourceTree = ""; }; 39924ED72BEBD22100139F91 /* CountlyCrashData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyCrashData.h; sourceTree = ""; }; 399B464F2C52813700AD384E /* CountlyLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyLocationTests.swift; sourceTree = ""; }; - 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; - 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacksNew.h; sourceTree = ""; }; + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacksNew.m; sourceTree = ""; }; 3B20A9822245225A00E3D7AE /* Countly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Countly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B20A9852245225A00E3D7AE /* Countly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Countly.h; sourceTree = ""; }; 3B20A9862245225A00E3D7AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -185,8 +185,8 @@ D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPerformanceMonitoring.m; sourceTree = ""; }; D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbackWidget.h; sourceTree = ""; }; D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbackWidget.m; sourceTree = ""; }; - D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacksInternal.h; sourceTree = ""; }; - D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacksInternal.m; sourceTree = ""; }; + D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; + D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -228,8 +228,8 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( - 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */, - 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */, + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */, + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */, 39002D092C8B2E450049394F /* CountlyContentConfig.h */, 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */, 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */, @@ -280,8 +280,8 @@ 3B20A9AA2245228500E3D7AE /* CountlyDeviceInfo.m */, 3B20A99E2245228400E3D7AE /* CountlyEvent.h */, 3B20A9952245228400E3D7AE /* CountlyEvent.m */, - D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */, - D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */, + D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */, + D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */, D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */, D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */, 3B20A99A2245228400E3D7AE /* CountlyLocationManager.h */, @@ -332,7 +332,7 @@ files = ( 39924ECE2BEBD0B700139F91 /* CountlyCrashesConfig.h in Headers */, 1A478D032AB314750056A5E7 /* CountlyExperimentInformation.h in Headers */, - 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */, + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacksNew.h in Headers */, 3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */, 3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */, 3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */, @@ -364,7 +364,7 @@ D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */, 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */, 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */, - D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */, + D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */, 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -496,11 +496,11 @@ 3B20A9D42245228700E3D7AE /* CountlyPersistency.m in Sources */, 3B20A9B32245228700E3D7AE /* CountlyNotificationService.m in Sources */, 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */, - D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */, + D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */, 1A3110632A7128CD001CB507 /* CountlyViewData.m in Sources */, 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */, 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */, - 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */, + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacksNew.m in Sources */, 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, diff --git a/CountlyCommon.h b/CountlyCommon.h index 69a29fce..6d3ef538 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -15,7 +15,7 @@ #import "CountlyCrashReporter.h" #import "CountlyConfig.h" #import "CountlyViewTrackingInternal.h" -#import "CountlyFeedbacksInternal.h" +#import "CountlyFeedbacksNewInternal.h" #import "CountlyFeedbackWidget.h" #import "CountlyPushNotifications.h" #import "CountlyNotificationService.h" diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 57f182b0..fc4bf12f 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -438,7 +438,7 @@ - (void)setConsentForFeedback:(BOOL)consentForFeedback { CLY_LOG_D(@"Consent for Feedback is given."); - [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; + [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; } else { diff --git a/CountlyFeedbacks.h b/CountlyFeedbacks.h index 446d1e55..4e774aea 100644 --- a/CountlyFeedbacks.h +++ b/CountlyFeedbacks.h @@ -1,30 +1,41 @@ -// CountlyFeedbacks.h +// CountlyFeedbacksInternal.h // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. -// #import -#import "CountlyConfig.h" -#import "CountlyFeedbackWidget.h" -@interface CountlyFeedbacks: NSObject +@class CountlyFeedbackWidget; + +extern NSString* const kCountlyFBKeyPlatform; +extern NSString* const kCountlyFBKeyAppVersion; +extern NSString* const kCountlyFBKeyWidgetID; +extern NSString* const kCountlyFBKeyID; + +extern NSString* const kCountlyReservedEventStarRating; + +@interface CountlyFeedbacks : NSObject #if (TARGET_OS_IOS) + (instancetype)sharedInstance; -- (void) presentNPS; -- (void) presentNPS:(NSString *)nameIDorTag; -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; +- (void)showDialog:(void(^)(NSInteger rating))completion; +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; +- (void)checkForStarRatingAutoAsk; + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; -- (void) presentSurvey; -- (void) presentSurvey:(NSString *)nameIDorTag; -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; -- (void) presentRating; -- (void) presentRating:(NSString *)nameIDorTag; -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; -- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler; +@property (nonatomic) NSString* message; +@property (nonatomic) NSString* dismissButtonTitle; +@property (nonatomic) NSUInteger sessionCount; +@property (nonatomic) BOOL disableAskingForEachAppVersion; +@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); #endif @end diff --git a/CountlyFeedbacks.m b/CountlyFeedbacks.m index 5b566c0d..8a04c1a6 100644 --- a/CountlyFeedbacks.m +++ b/CountlyFeedbacks.m @@ -1,20 +1,57 @@ -// CountlyFeedbacks.m +// CountlyFeedbacks.m // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. -// -#import "CountlyFeedbacks.h" #import "CountlyCommon.h" +#if (TARGET_OS_IOS) +#import +#endif + +@interface CountlyFeedbackWidget () ++ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; +@end + + + +@interface CountlyFeedbacks () +#if (TARGET_OS_IOS) +@property (nonatomic) UIAlertController* alertController; +@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); +#endif +@end + +NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; +NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; +NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; + +NSString* const kCountlyFBKeyPlatform = @"platform"; +NSString* const kCountlyFBKeyAppVersion = @"app_version"; +NSString* const kCountlyFBKeyRating = @"rating"; +NSString* const kCountlyFBKeyWidgetID = @"widget_id"; +NSString* const kCountlyFBKeyID = @"_id"; +NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; +NSString* const kCountlyFBKeyPhone = @"phone"; +NSString* const kCountlyFBKeyTablet = @"tablet"; +NSString* const kCountlyFBKeyFeedback = @"feedback"; +NSString* const kCountlyFBKeyEmail = @"email"; +NSString* const kCountlyFBKeyComment = @"comment"; +NSString* const kCountlyFBKeyContactMe = @"contactMe"; + +const CGFloat kCountlyStarRatingButtonSize = 40.0; @implementation CountlyFeedbacks #if (TARGET_OS_IOS) +{ + UIButton* btn_star[5]; +} + + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) return nil; - + static CountlyFeedbacks* s_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); @@ -23,56 +60,509 @@ + (instancetype)sharedInstance - (instancetype)init { - self = [super init]; - + if (self = [super init]) + { + NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; + + NSDictionary* dictMessage = + @{ + @"en": @"How would you rate the app?", + @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", + @"ja": @"あなたの評価を教えてください。", + @"zh": @"请告诉我你的评价。", + @"ru": @"Как бы вы оценили приложение?", + @"cz": @"Jak hodnotíte aplikaci?", + @"lv": @"Kā Jūs novērtētu šo lietotni?", + @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", + @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", + }; + + self.message = dictMessage[langDesignator]; + if (!self.message) + self.message = dictMessage[@"en"]; + } + return self; } -- (void)enterContentZone:(NSArray *)tags +#pragma mark - Star Rating + +- (void)showDialog:(void(^)(NSInteger rating))completion { - [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + self.ratingCompletion = completion; + + self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton]; + dismissButton.onClick = ^(id sender) + { + [self.alertController dismissViewControllerAnimated:YES completion:^ + { + [self finishWithRating:0]; + }]; + }; + [self.alertController.view addSubview:dismissButton]; + [dismissButton positionToTopRight]; + + CLYInternalViewController* cvc = CLYInternalViewController.new; + [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; + [cvc.view addSubview:[self starView]]; + + @try + { + [self.alertController setValue:cvc forKey:@"contentViewController"]; + } + @catch (NSException* exception) + { + CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); + } + + [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; } -- (void)presentNPS { - [self presentNPS:nil widgetCallback:nil]; +- (void)checkForStarRatingAutoAsk +{ + if (!self.sessionCount) + return; + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; + + if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) + return; + + NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; + NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; + sessionCountSoFar++; + + if (self.sessionCount == sessionCountSoFar) + { + CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); + + [self showDialog:self.ratingCompletionForAutoAsk]; + + status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; + } + + status[keyForAppVersion] = @(sessionCountSoFar); + + [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; } -- (void)presentNPS:(NSString *)nameIDorTag { - [self presentNPS:nameIDorTag widgetCallback:nil]; +- (UIView *)starView +{ + UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; + vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + + for (int i = 0; i < 5; i++) + { + btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; + btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; + [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; + [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; + [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; + + [vw_star addSubview:btn_star[i]]; + } + + return vw_star; } -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; +- (void)setMessage:(NSString *)message +{ + if (!message) + return; + + _message = message; } -- (void)presentSurvey { - [self presentSurvey:nil widgetCallback:nil]; +- (void)onClick_star:(id)sender +{ + UIColor* color = [self activeStarColor]; + NSInteger rating = 0; + + for (int i = 0; i < 5; i++) + { + [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; + + if (btn_star[i] == sender) + { + color = [self passiveStarColor]; + rating = i + 1; + } + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ + { + [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; + }); } -- (void)presentSurvey:(NSString *)nameIDorTag { - [self presentSurvey:nameIDorTag widgetCallback:nil]; +- (void)finishWithRating:(NSInteger)rating +{ + if (self.ratingCompletion) + self.ratingCompletion(rating); + + if (rating != 0) + { + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; + } + + self.alertController = nil; + self.ratingCompletion = nil; } -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; +- (UIColor *)activeStarColor +{ + return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; +} + +- (UIColor *)passiveStarColor +{ + return [UIColor colorWithWhite:178/255.0 alpha:1]; +} + +#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) + +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + if (!widgetID.length) + return; + + NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary* widgetInfo = nil; + + if (!error) + { + widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + NSMutableDictionary* userInfo = widgetInfo.mutableCopy; + + if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; + } + else if (![self isDeviceTargetedByWidget:widgetInfo]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(error); + }); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + }); + }]; + + [task resume]; } -- (void)presentRating { - [self presentRating:nil widgetCallback:nil]; +- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + __block CLYInternalViewController* webVC = CLYInternalViewController.new; + webVC.view.backgroundColor = UIColor.whiteColor; + webVC.view.bounds = UIScreen.mainScreen.bounds; + webVC.modalPresentationStyle = UIModalPresentationCustom; + + WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; + webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [webVC.view addSubview:webView]; + NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; + [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; + dismissButton.onClick = ^(id sender) + { + [webVC dismissViewControllerAnimated:YES completion:^ + { + if (completionHandler) + completionHandler(nil); + + webVC = nil; + }]; + }; + [webVC.view addSubview:dismissButton]; + [dismissButton positionToTopRightConsideringStatusBar]; + + [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; +} + +- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", + kCountlyEndpointO, + kCountlyEndpointFeedback, + kCountlyEndpointWidget]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; + return request; + } } -- (void)presentRating:(NSString *)nameIDorTag { - [self presentRating:nameIDorTag widgetCallback:nil]; +- (NSURL *)widgetDisplayURL:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", + kCountlyFBKeyWidgetID, widgetID, + kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointFeedback, + queryString]; + + return [NSURL URLWithString:URLString]; } -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; +- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo +{ + BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; + BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; + BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; + BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; + + return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); } -- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted { - CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (!widgetID.length) + return; + + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + segmentation[kCountlyFBKeyWidgetID] = widgetID; + segmentation[kCountlyFBKeyEmail] = email; + segmentation[kCountlyFBKeyComment] = comment; + segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; } + + +#pragma mark - Feedbacks (Surveys, NPS) + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary *feedbacksResponse = nil; + + if (!error) + { + feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + if (((NSHTTPURLResponse*)response).statusCode != 200) + { + NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; + userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(nil, error); + }); + + return; + } + + NSMutableArray* feedbacks = NSMutableArray.new; + NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; + for (NSDictionary * feedbackDict in rawFeedbackObjects) + { + CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; + if (feedback) + [feedbacks addObject:feedback]; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler([NSArray arrayWithArray:feedbacks], nil); + }); + }]; + + [task resume]; +} + +- (NSURLRequest *)feedbacksRequest +{ + NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", + kCountlyQSKeyMethod, kCountlyFBKeyFeedback, + kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, + kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, + kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; + [URL appendString:kCountlyEndpointO]; + [URL appendString:kCountlyEndpointSDK]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + [URL appendFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; + return request; + } +} + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + + +-(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { + if (error) { + CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); + return; + } + + CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); + + NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; + NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; + + CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); + + CountlyFeedbackWidget *widgetToPresent = nil; + + if (nameIDorTag && nameIDorTag.length > 0) { + for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { + if ([nameIDorTag isEqualToString:feedbackWidget.name] || + [nameIDorTag isEqualToString:feedbackWidget.ID] || + [feedbackWidget.tags containsObject:nameIDorTag]) { + widgetToPresent = feedbackWidget; + CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); + break; + } + } + } + + if (!widgetToPresent && filteredWidgets.count > 0) { + widgetToPresent = filteredWidgets.firstObject; + CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); + } + + if (widgetToPresent) { + [widgetToPresent presentWithCallback:widgetCallback]; + } else { + CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); + } + }]; +} + #endif @end diff --git a/CountlyFeedbacksInternal.h b/CountlyFeedbacksInternal.h deleted file mode 100644 index 85518ebe..00000000 --- a/CountlyFeedbacksInternal.h +++ /dev/null @@ -1,41 +0,0 @@ -// CountlyFeedbacksInternal.h -// -// This code is provided under the MIT License. -// -// Please visit www.count.ly for more information. - -#import - -@class CountlyFeedbackWidget; - -extern NSString* const kCountlyFBKeyPlatform; -extern NSString* const kCountlyFBKeyAppVersion; -extern NSString* const kCountlyFBKeyWidgetID; -extern NSString* const kCountlyFBKeyID; - -extern NSString* const kCountlyReservedEventStarRating; - -@interface CountlyFeedbacksInternal : NSObject -#if (TARGET_OS_IOS) -+ (instancetype)sharedInstance; - -- (void)showDialog:(void(^)(NSInteger rating))completion; -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; -- (void)checkForStarRatingAutoAsk; - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; - -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; - -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; - -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; - -@property (nonatomic) NSString* message; -@property (nonatomic) NSString* dismissButtonTitle; -@property (nonatomic) NSUInteger sessionCount; -@property (nonatomic) BOOL disableAskingForEachAppVersion; -@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); -#endif -@end diff --git a/CountlyFeedbacksInternal.m b/CountlyFeedbacksInternal.m deleted file mode 100644 index 4a7bb9e2..00000000 --- a/CountlyFeedbacksInternal.m +++ /dev/null @@ -1,568 +0,0 @@ -// CountlyFeedbacks.m -// -// This code is provided under the MIT License. -// -// Please visit www.count.ly for more information. - -#import "CountlyCommon.h" -#if (TARGET_OS_IOS) -#import -#endif - -@interface CountlyFeedbackWidget () -+ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; -@end - - - -@interface CountlyFeedbacksInternal () -#if (TARGET_OS_IOS) -@property (nonatomic) UIAlertController* alertController; -@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); -#endif -@end - -NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; -NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; -NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; - -NSString* const kCountlyFBKeyPlatform = @"platform"; -NSString* const kCountlyFBKeyAppVersion = @"app_version"; -NSString* const kCountlyFBKeyRating = @"rating"; -NSString* const kCountlyFBKeyWidgetID = @"widget_id"; -NSString* const kCountlyFBKeyID = @"_id"; -NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; -NSString* const kCountlyFBKeyPhone = @"phone"; -NSString* const kCountlyFBKeyTablet = @"tablet"; -NSString* const kCountlyFBKeyFeedback = @"feedback"; -NSString* const kCountlyFBKeyEmail = @"email"; -NSString* const kCountlyFBKeyComment = @"comment"; -NSString* const kCountlyFBKeyContactMe = @"contactMe"; - -const CGFloat kCountlyStarRatingButtonSize = 40.0; - -@implementation CountlyFeedbacksInternal -#if (TARGET_OS_IOS) -{ - UIButton* btn_star[5]; -} - -+ (instancetype)sharedInstance -{ - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; - - static CountlyFeedbacksInternal* s_sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); - return s_sharedInstance; -} - -- (instancetype)init -{ - if (self = [super init]) - { - NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; - - NSDictionary* dictMessage = - @{ - @"en": @"How would you rate the app?", - @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", - @"ja": @"あなたの評価を教えてください。", - @"zh": @"请告诉我你的评价。", - @"ru": @"Как бы вы оценили приложение?", - @"cz": @"Jak hodnotíte aplikaci?", - @"lv": @"Kā Jūs novērtētu šo lietotni?", - @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", - @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", - }; - - self.message = dictMessage[langDesignator]; - if (!self.message) - self.message = dictMessage[@"en"]; - } - - return self; -} - -#pragma mark - Star Rating - -- (void)showDialog:(void(^)(NSInteger rating))completion -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - self.ratingCompletion = completion; - - self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton]; - dismissButton.onClick = ^(id sender) - { - [self.alertController dismissViewControllerAnimated:YES completion:^ - { - [self finishWithRating:0]; - }]; - }; - [self.alertController.view addSubview:dismissButton]; - [dismissButton positionToTopRight]; - - CLYInternalViewController* cvc = CLYInternalViewController.new; - [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; - [cvc.view addSubview:[self starView]]; - - @try - { - [self.alertController setValue:cvc forKey:@"contentViewController"]; - } - @catch (NSException* exception) - { - CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); - } - - [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; -} - -- (void)checkForStarRatingAutoAsk -{ - if (!self.sessionCount) - return; - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; - - if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) - return; - - NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; - NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; - sessionCountSoFar++; - - if (self.sessionCount == sessionCountSoFar) - { - CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); - - [self showDialog:self.ratingCompletionForAutoAsk]; - - status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; - } - - status[keyForAppVersion] = @(sessionCountSoFar); - - [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; -} - -- (UIView *)starView -{ - UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; - vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; - - for (int i = 0; i < 5; i++) - { - btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; - btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; - [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; - [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; - [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; - - [vw_star addSubview:btn_star[i]]; - } - - return vw_star; -} - -- (void)setMessage:(NSString *)message -{ - if (!message) - return; - - _message = message; -} - -- (void)onClick_star:(id)sender -{ - UIColor* color = [self activeStarColor]; - NSInteger rating = 0; - - for (int i = 0; i < 5; i++) - { - [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; - - if (btn_star[i] == sender) - { - color = [self passiveStarColor]; - rating = i + 1; - } - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ - { - [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; - }); -} - -- (void)finishWithRating:(NSInteger)rating -{ - if (self.ratingCompletion) - self.ratingCompletion(rating); - - if (rating != 0) - { - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; - } - - self.alertController = nil; - self.ratingCompletion = nil; -} - -- (UIColor *)activeStarColor -{ - return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; -} - -- (UIColor *)passiveStarColor -{ - return [UIColor colorWithWhite:178/255.0 alpha:1]; -} - -#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) - -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - if (!widgetID.length) - return; - - NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary* widgetInfo = nil; - - if (!error) - { - widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - NSMutableDictionary* userInfo = widgetInfo.mutableCopy; - - if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; - } - else if (![self isDeviceTargetedByWidget:widgetInfo]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(error); - }); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; - }); - }]; - - [task resume]; -} - -- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler -{ - __block CLYInternalViewController* webVC = CLYInternalViewController.new; - webVC.view.backgroundColor = UIColor.whiteColor; - webVC.view.bounds = UIScreen.mainScreen.bounds; - webVC.modalPresentationStyle = UIModalPresentationCustom; - - WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; - webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [webVC.view addSubview:webView]; - NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; - [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; - dismissButton.onClick = ^(id sender) - { - [webVC dismissViewControllerAnimated:YES completion:^ - { - if (completionHandler) - completionHandler(nil); - - webVC = nil; - }]; - }; - [webVC.view addSubview:dismissButton]; - [dismissButton positionToTopRightConsideringStatusBar]; - - [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; -} - -- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", - kCountlyEndpointO, - kCountlyEndpointFeedback, - kCountlyEndpointWidget]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; - return request; - } -} - -- (NSURL *)widgetDisplayURL:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", - kCountlyFBKeyWidgetID, widgetID, - kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", - CountlyConnectionManager.sharedInstance.host, - kCountlyEndpointFeedback, - queryString]; - - return [NSURL URLWithString:URLString]; -} - -- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo -{ - BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; - BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; - BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; - BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; - - return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); -} - -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (!widgetID.length) - return; - - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - segmentation[kCountlyFBKeyWidgetID] = widgetID; - segmentation[kCountlyFBKeyEmail] = email; - segmentation[kCountlyFBKeyComment] = comment; - segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; -} - - -#pragma mark - Feedbacks (Surveys, NPS) - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary *feedbacksResponse = nil; - - if (!error) - { - feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - if (((NSHTTPURLResponse*)response).statusCode != 200) - { - NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; - userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(nil, error); - }); - - return; - } - - NSMutableArray* feedbacks = NSMutableArray.new; - NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; - for (NSDictionary * feedbackDict in rawFeedbackObjects) - { - CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; - if (feedback) - [feedbacks addObject:feedback]; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler([NSArray arrayWithArray:feedbacks], nil); - }); - }]; - - [task resume]; -} - -- (NSURLRequest *)feedbacksRequest -{ - NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", - kCountlyQSKeyMethod, kCountlyFBKeyFeedback, - kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, - kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, - kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointO]; - [URL appendString:kCountlyEndpointSDK]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - [URL appendFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; - } -} - -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; -} - -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; -} - -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; -} - - --(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { - if (error) { - CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); - return; - } - - CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); - - NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; - NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; - - CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); - - CountlyFeedbackWidget *widgetToPresent = nil; - - if (nameIDorTag && nameIDorTag.length > 0) { - for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { - if ([nameIDorTag isEqualToString:feedbackWidget.name] || - [nameIDorTag isEqualToString:feedbackWidget.ID] || - [feedbackWidget.tags containsObject:nameIDorTag]) { - widgetToPresent = feedbackWidget; - CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); - break; - } - } - } - - if (!widgetToPresent && filteredWidgets.count > 0) { - widgetToPresent = filteredWidgets.firstObject; - CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); - } - - if (widgetToPresent) { - [widgetToPresent presentWithCallback:widgetCallback]; - } else { - CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); - } - }]; -} - -#endif -@end diff --git a/CountlyFeedbacksNew.h b/CountlyFeedbacksNew.h new file mode 100644 index 00000000..1c253cd9 --- /dev/null +++ b/CountlyFeedbacksNew.h @@ -0,0 +1,30 @@ +// CountlyFeedbacks.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. +// + +#import +#import "CountlyConfig.h" +#import "CountlyFeedbackWidget.h" + +@interface CountlyFeedbacksNew: NSObject +#if (TARGET_OS_IOS) ++ (instancetype)sharedInstance; + +- (void) presentNPS; +- (void) presentNPS:(NSString *)nameIDorTag; +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; + +- (void) presentSurvey; +- (void) presentSurvey:(NSString *)nameIDorTag; +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; + +- (void) presentRating; +- (void) presentRating:(NSString *)nameIDorTag; +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; + +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler; +#endif +@end diff --git a/CountlyFeedbacksNew.m b/CountlyFeedbacksNew.m new file mode 100644 index 00000000..766681ab --- /dev/null +++ b/CountlyFeedbacksNew.m @@ -0,0 +1,78 @@ +// CountlyFeedbacks.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. +// + +#import "CountlyFeedbacksNew.h" +#import "CountlyCommon.h" + +@implementation CountlyFeedbacksNew +#if (TARGET_OS_IOS) ++ (instancetype)sharedInstance +{ + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + + static CountlyFeedbacksNew* s_sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; +} + +- (instancetype)init +{ + self = [super init]; + + return self; +} + +- (void)enterContentZone:(NSArray *)tags +{ + [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; +} + +- (void)presentNPS { + [self presentNPS:nil widgetCallback:nil]; +} + +- (void)presentNPS:(NSString *)nameIDorTag { + [self presentNPS:nameIDorTag widgetCallback:nil]; +} + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void)presentSurvey { + [self presentSurvey:nil widgetCallback:nil]; +} + +- (void)presentSurvey:(NSString *)nameIDorTag { + [self presentSurvey:nameIDorTag widgetCallback:nil]; +} + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void)presentRating { + [self presentRating:nil widgetCallback:nil]; +} + +- (void)presentRating:(NSString *)nameIDorTag { + [self presentRating:nameIDorTag widgetCallback:nil]; +} + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; +} +#endif +@end From 510e4a76e2990a62af820ef08f10ba6be60246a9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 20:12:53 +0500 Subject: [PATCH 107/148] Fixed typos --- CountlyFeedbackWidget.h | 4 ++-- CountlyFeedbacks.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CountlyFeedbackWidget.h b/CountlyFeedbackWidget.h index 9bce328b..5ae8f0fe 100644 --- a/CountlyFeedbackWidget.h +++ b/CountlyFeedbackWidget.h @@ -45,9 +45,9 @@ extern NSString* const kCountlyReservedEventRating; /** * Modally presents the feedback widget above the top visible view controller and executes given blocks. * @discussion Calls to this method will be ignored if consent for @c CLYConsentFeedback is not given while @c requiresConsent flag is set on initial configuration. - * @param wigetCallback Block to be executed when widget is displayed/dismissed + * @param widgetCallback Block to be executed when widget is displayed/dismissed */ -- (void)presentWithCallback:(WidgetCallback) wigetCallback; +- (void)presentWithCallback:(WidgetCallback) widgetCallback; /** * Fetches feedback widget's data to be used for manually presenting it. * @discussion When feedback widget's data is fetched successfully, @c completionHandler will be executed with an @c NSDictionary diff --git a/CountlyFeedbacks.h b/CountlyFeedbacks.h index 4e774aea..4518908f 100644 --- a/CountlyFeedbacks.h +++ b/CountlyFeedbacks.h @@ -26,11 +26,11 @@ extern NSString* const kCountlyReservedEventStarRating; - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; @property (nonatomic) NSString* message; @property (nonatomic) NSString* dismissButtonTitle; From a7078d09c098966a091bbf3009678fb6aab84705 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 22 Oct 2024 22:37:45 +0500 Subject: [PATCH 108/148] Fixed renaming issues --- CountlyCommon.h | 2 +- CountlyFeedbacksNew.m | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CountlyCommon.h b/CountlyCommon.h index 6d3ef538..3a1f4564 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -15,7 +15,7 @@ #import "CountlyCrashReporter.h" #import "CountlyConfig.h" #import "CountlyViewTrackingInternal.h" -#import "CountlyFeedbacksNewInternal.h" +#import "CountlyFeedbacks.h" #import "CountlyFeedbackWidget.h" #import "CountlyPushNotifications.h" #import "CountlyNotificationService.h" diff --git a/CountlyFeedbacksNew.m b/CountlyFeedbacksNew.m index 766681ab..7d869ed2 100644 --- a/CountlyFeedbacksNew.m +++ b/CountlyFeedbacksNew.m @@ -42,7 +42,7 @@ - (void)presentNPS:(NSString *)nameIDorTag { } - (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; + [CountlyFeedbacks.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; } - (void)presentSurvey { @@ -54,7 +54,7 @@ - (void)presentSurvey:(NSString *)nameIDorTag { } - (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; + [CountlyFeedbacks.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; } - (void)presentRating { @@ -66,13 +66,13 @@ - (void)presentRating:(NSString *)nameIDorTag { } - (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacksInternal.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; + [CountlyFeedbacks.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; } - (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; + [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; } #endif @end From a6e1d0ad551c40614dee71bae6be30825c7c08ee Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 12:20:16 +0500 Subject: [PATCH 109/148] some fixes --- Countly.m | 5 +++++ CountlyFeedbacks.m | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Countly.m b/Countly.m index d6e46173..d6ed80bd 100644 --- a/Countly.m +++ b/Countly.m @@ -1284,6 +1284,11 @@ - (CountlyContentBuilder *) content return CountlyContentBuilder.sharedInstance; } +- (CountlyFeedbacksNew *) feedback +{ + return CountlyFeedbacksNew.sharedInstance; +} + #endif diff --git a/CountlyFeedbacks.m b/CountlyFeedbacks.m index 8a04c1a6..bd8ffa56 100644 --- a/CountlyFeedbacks.m +++ b/CountlyFeedbacks.m @@ -524,7 +524,7 @@ - (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) w -(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { + [self getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { if (error) { CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); return; From dab794c69a12363b1c804c369a85172442aafe9b Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 12:33:57 +0500 Subject: [PATCH 110/148] Temp renaming reverted --- Countly.h | 4 +- Countly.m | 22 +- Countly.xcodeproj/project.pbxproj | 32 +- CountlyCommon.h | 2 +- CountlyConsentManager.m | 2 +- CountlyFeedbacks.h | 35 +- CountlyFeedbacks.m | 548 ++-------------------------- CountlyFeedbacksInternal.h | 41 +++ CountlyFeedbacksInternal.m | 568 ++++++++++++++++++++++++++++++ CountlyFeedbacksNew.h | 30 -- 10 files changed, 681 insertions(+), 603 deletions(-) create mode 100644 CountlyFeedbacksInternal.h create mode 100644 CountlyFeedbacksInternal.m delete mode 100644 CountlyFeedbacksNew.h diff --git a/Countly.h b/Countly.h index b0957fec..a4a337fd 100644 --- a/Countly.h +++ b/Countly.h @@ -13,7 +13,7 @@ #import "CountlyFeedbackWidget.h" #import "CountlyViewTracking.h" #import "CountlyContentBuilder.h" -#import "CountlyFeedbacksNew.h" +#import "CountlyFeedbacks.h" #import "Resettable.h" #if (TARGET_OS_IOS || TARGET_OS_OSX) #import @@ -690,7 +690,7 @@ NS_ASSUME_NONNULL_BEGIN * Interface variable to access feedback widget functionalities. * @discussion Feedback widget interface for developer to interact with SDK. */ -- (CountlyFeedbacksNew *) feedback; +- (CountlyFeedbacks *) feedback; #endif diff --git a/Countly.m b/Countly.m index d6ed80bd..947616e9 100644 --- a/Countly.m +++ b/Countly.m @@ -175,11 +175,11 @@ - (void)startWithConfig:(CountlyConfig *)config } #if (TARGET_OS_IOS) - CountlyFeedbacks.sharedInstance.message = config.starRatingMessage; - CountlyFeedbacks.sharedInstance.sessionCount = config.starRatingSessionCount; - CountlyFeedbacks.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; - CountlyFeedbacks.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; - [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; + CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; + CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; + CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; + CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; #endif if(config.disableLocation) @@ -1240,7 +1240,7 @@ - (void)askForStarRating:(void(^)(NSInteger rating))completion { CLY_LOG_I(@"%s %@", __FUNCTION__, completion); - [CountlyFeedbacks.sharedInstance showDialog:completion]; + [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; } - (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler @@ -1262,21 +1262,21 @@ - (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString CLY_LOG_I(@"%s %@ %@ %@", __FUNCTION__, widgetID, closeButtonText, completionHandler); - [CountlyFeedbacks.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; } - (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted { CLY_LOG_I(@"%s %@ %ld %@ %@ %d", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); - [CountlyFeedbacks.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; + [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; } - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; } - (CountlyContentBuilder *) content @@ -1284,9 +1284,9 @@ - (CountlyContentBuilder *) content return CountlyContentBuilder.sharedInstance; } -- (CountlyFeedbacksNew *) feedback +- (CountlyFeedbacks *) feedback { - return CountlyFeedbacksNew.sharedInstance; + return CountlyFeedbacks.sharedInstance; } #endif diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index ff5a1072..d6bc336f 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -48,8 +48,8 @@ 39924ED62BEBD20F00139F91 /* CountlyCrashData.m in Sources */ = {isa = PBXBuildFile; fileRef = 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */; }; 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */ = {isa = PBXBuildFile; fileRef = 39924ED72BEBD22100139F91 /* CountlyCrashData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 399B46502C52813700AD384E /* CountlyLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399B464F2C52813700AD384E /* CountlyLocationTests.swift */; }; - 39BDF7572CC7CA870066DE7C /* CountlyFeedbacksNew.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 39BDF7592CC7CA920066DE7C /* CountlyFeedbacksNew.m in Sources */ = {isa = PBXBuildFile; fileRef = 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */; }; + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */; }; 39EE1F102C8B341E0016D1BF /* CountlyExperimentalConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3903429B2C8051B400238C96 /* CountlyExperimentalConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9872245225A00E3D7AE /* Countly.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9852245225A00E3D7AE /* Countly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B20A9B22245228700E3D7AE /* CountlyConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A98D2245228300E3D7AE /* CountlyConnectionManager.h */; }; @@ -87,8 +87,8 @@ D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */ = {isa = PBXBuildFile; fileRef = D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */; }; D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; D249BF5F254D3D180058A6C2 /* CountlyFeedbackWidget.m in Sources */ = {isa = PBXBuildFile; fileRef = D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */; }; - D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */; }; - D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */; }; + D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */; }; + D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -145,8 +145,8 @@ 39924ED52BEBD20F00139F91 /* CountlyCrashData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyCrashData.m; sourceTree = ""; }; 39924ED72BEBD22100139F91 /* CountlyCrashData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyCrashData.h; sourceTree = ""; }; 399B464F2C52813700AD384E /* CountlyLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyLocationTests.swift; sourceTree = ""; }; - 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacksNew.h; sourceTree = ""; }; - 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacksNew.m; sourceTree = ""; }; + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; 3B20A9822245225A00E3D7AE /* Countly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Countly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B20A9852245225A00E3D7AE /* Countly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Countly.h; sourceTree = ""; }; 3B20A9862245225A00E3D7AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -185,8 +185,8 @@ D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPerformanceMonitoring.m; sourceTree = ""; }; D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbackWidget.h; sourceTree = ""; }; D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbackWidget.m; sourceTree = ""; }; - D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacks.h; sourceTree = ""; }; - D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacks.m; sourceTree = ""; }; + D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbacksInternal.h; sourceTree = ""; }; + D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyFeedbacksInternal.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -228,8 +228,8 @@ 3B20A9782245225A00E3D7AE = { isa = PBXGroup; children = ( - 39BDF7562CC7CA870066DE7C /* CountlyFeedbacksNew.h */, - 39BDF7582CC7CA920066DE7C /* CountlyFeedbacksNew.m */, + 39BDF7562CC7CA870066DE7C /* CountlyFeedbacks.h */, + 39BDF7582CC7CA920066DE7C /* CountlyFeedbacks.m */, 39002D092C8B2E450049394F /* CountlyContentConfig.h */, 39002D0A2C8B2E450049394F /* CountlyContentConfig.m */, 3961C6AF2C6633C000DD38BA /* CountlyWebViewManager.h */, @@ -280,8 +280,8 @@ 3B20A9AA2245228500E3D7AE /* CountlyDeviceInfo.m */, 3B20A99E2245228400E3D7AE /* CountlyEvent.h */, 3B20A9952245228400E3D7AE /* CountlyEvent.m */, - D2CFEF952545FBE80026B044 /* CountlyFeedbacks.h */, - D2CFEF962545FBE80026B044 /* CountlyFeedbacks.m */, + D2CFEF952545FBE80026B044 /* CountlyFeedbacksInternal.h */, + D2CFEF962545FBE80026B044 /* CountlyFeedbacksInternal.m */, D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */, D249BF5D254D3D180058A6C2 /* CountlyFeedbackWidget.m */, 3B20A99A2245228400E3D7AE /* CountlyLocationManager.h */, @@ -332,7 +332,7 @@ files = ( 39924ECE2BEBD0B700139F91 /* CountlyCrashesConfig.h in Headers */, 1A478D032AB314750056A5E7 /* CountlyExperimentInformation.h in Headers */, - 39BDF7572CC7CA870066DE7C /* CountlyFeedbacksNew.h in Headers */, + 39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */, 3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */, 3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */, 3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */, @@ -364,7 +364,7 @@ D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */, 39924ED82BEBD22100139F91 /* CountlyCrashData.h in Headers */, 399117D22C69F73D00DC4C66 /* CountlyContentBuilderInternal.h in Headers */, - D2CFEF972545FBE80026B044 /* CountlyFeedbacks.h in Headers */, + D2CFEF972545FBE80026B044 /* CountlyFeedbacksInternal.h in Headers */, 39002D0B2C8B2E450049394F /* CountlyContentConfig.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -496,11 +496,11 @@ 3B20A9D42245228700E3D7AE /* CountlyPersistency.m in Sources */, 3B20A9B32245228700E3D7AE /* CountlyNotificationService.m in Sources */, 3948A8572BAC2E7D002D09AA /* CountlySDKLimitsConfig.m in Sources */, - D2CFEF982545FBE80026B044 /* CountlyFeedbacks.m in Sources */, + D2CFEF982545FBE80026B044 /* CountlyFeedbacksInternal.m in Sources */, 1A3110632A7128CD001CB507 /* CountlyViewData.m in Sources */, 3961C6B92C6633C000DD38BA /* PassThroughBackgroundView.m in Sources */, 399117D32C69F73D00DC4C66 /* CountlyContentBuilderInternal.m in Sources */, - 39BDF7592CC7CA920066DE7C /* CountlyFeedbacksNew.m in Sources */, + 39BDF7592CC7CA920066DE7C /* CountlyFeedbacks.m in Sources */, 1A423E9E2A271E46008C4757 /* CountlyRCData.m in Sources */, 3B20A9CB2245228700E3D7AE /* CountlyRemoteConfig.m in Sources */, 3B20A9BD2245228700E3D7AE /* CountlyConnectionManager.m in Sources */, diff --git a/CountlyCommon.h b/CountlyCommon.h index 3a1f4564..69a29fce 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -15,7 +15,7 @@ #import "CountlyCrashReporter.h" #import "CountlyConfig.h" #import "CountlyViewTrackingInternal.h" -#import "CountlyFeedbacks.h" +#import "CountlyFeedbacksInternal.h" #import "CountlyFeedbackWidget.h" #import "CountlyPushNotifications.h" #import "CountlyNotificationService.h" diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index fc4bf12f..57f182b0 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -438,7 +438,7 @@ - (void)setConsentForFeedback:(BOOL)consentForFeedback { CLY_LOG_D(@"Consent for Feedback is given."); - [CountlyFeedbacks.sharedInstance checkForStarRatingAutoAsk]; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; } else { diff --git a/CountlyFeedbacks.h b/CountlyFeedbacks.h index 4518908f..446d1e55 100644 --- a/CountlyFeedbacks.h +++ b/CountlyFeedbacks.h @@ -1,41 +1,30 @@ -// CountlyFeedbacksInternal.h +// CountlyFeedbacks.h // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. +// #import +#import "CountlyConfig.h" +#import "CountlyFeedbackWidget.h" -@class CountlyFeedbackWidget; - -extern NSString* const kCountlyFBKeyPlatform; -extern NSString* const kCountlyFBKeyAppVersion; -extern NSString* const kCountlyFBKeyWidgetID; -extern NSString* const kCountlyFBKeyID; - -extern NSString* const kCountlyReservedEventStarRating; - -@interface CountlyFeedbacks : NSObject +@interface CountlyFeedbacks: NSObject #if (TARGET_OS_IOS) + (instancetype)sharedInstance; -- (void)showDialog:(void(^)(NSInteger rating))completion; -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; -- (void)checkForStarRatingAutoAsk; - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; - +- (void) presentNPS; +- (void) presentNPS:(NSString *)nameIDorTag; - (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; +- (void) presentSurvey; +- (void) presentSurvey:(NSString *)nameIDorTag; - (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; +- (void) presentRating; +- (void) presentRating:(NSString *)nameIDorTag; - (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; -@property (nonatomic) NSString* message; -@property (nonatomic) NSString* dismissButtonTitle; -@property (nonatomic) NSUInteger sessionCount; -@property (nonatomic) BOOL disableAskingForEachAppVersion; -@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler; #endif @end diff --git a/CountlyFeedbacks.m b/CountlyFeedbacks.m index bd8ffa56..5b566c0d 100644 --- a/CountlyFeedbacks.m +++ b/CountlyFeedbacks.m @@ -1,57 +1,20 @@ -// CountlyFeedbacks.m +// CountlyFeedbacks.m // // This code is provided under the MIT License. // // Please visit www.count.ly for more information. +// +#import "CountlyFeedbacks.h" #import "CountlyCommon.h" -#if (TARGET_OS_IOS) -#import -#endif - -@interface CountlyFeedbackWidget () -+ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; -@end - - - -@interface CountlyFeedbacks () -#if (TARGET_OS_IOS) -@property (nonatomic) UIAlertController* alertController; -@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); -#endif -@end - -NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; -NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; -NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; - -NSString* const kCountlyFBKeyPlatform = @"platform"; -NSString* const kCountlyFBKeyAppVersion = @"app_version"; -NSString* const kCountlyFBKeyRating = @"rating"; -NSString* const kCountlyFBKeyWidgetID = @"widget_id"; -NSString* const kCountlyFBKeyID = @"_id"; -NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; -NSString* const kCountlyFBKeyPhone = @"phone"; -NSString* const kCountlyFBKeyTablet = @"tablet"; -NSString* const kCountlyFBKeyFeedback = @"feedback"; -NSString* const kCountlyFBKeyEmail = @"email"; -NSString* const kCountlyFBKeyComment = @"comment"; -NSString* const kCountlyFBKeyContactMe = @"contactMe"; - -const CGFloat kCountlyStarRatingButtonSize = 40.0; @implementation CountlyFeedbacks #if (TARGET_OS_IOS) -{ - UIButton* btn_star[5]; -} - + (instancetype)sharedInstance { if (!CountlyCommon.sharedInstance.hasStarted) return nil; - + static CountlyFeedbacks* s_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); @@ -60,509 +23,56 @@ + (instancetype)sharedInstance - (instancetype)init { - if (self = [super init]) - { - NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; - - NSDictionary* dictMessage = - @{ - @"en": @"How would you rate the app?", - @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", - @"ja": @"あなたの評価を教えてください。", - @"zh": @"请告诉我你的评价。", - @"ru": @"Как бы вы оценили приложение?", - @"cz": @"Jak hodnotíte aplikaci?", - @"lv": @"Kā Jūs novērtētu šo lietotni?", - @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", - @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", - }; - - self.message = dictMessage[langDesignator]; - if (!self.message) - self.message = dictMessage[@"en"]; - } - - return self; -} - -#pragma mark - Star Rating - -- (void)showDialog:(void(^)(NSInteger rating))completion -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - self.ratingCompletion = completion; - - self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton]; - dismissButton.onClick = ^(id sender) - { - [self.alertController dismissViewControllerAnimated:YES completion:^ - { - [self finishWithRating:0]; - }]; - }; - [self.alertController.view addSubview:dismissButton]; - [dismissButton positionToTopRight]; - - CLYInternalViewController* cvc = CLYInternalViewController.new; - [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; - [cvc.view addSubview:[self starView]]; - - @try - { - [self.alertController setValue:cvc forKey:@"contentViewController"]; - } - @catch (NSException* exception) - { - CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); - } - - [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; -} - -- (void)checkForStarRatingAutoAsk -{ - if (!self.sessionCount) - return; - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; - - if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) - return; - - NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; - NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; - sessionCountSoFar++; - - if (self.sessionCount == sessionCountSoFar) - { - CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); - - [self showDialog:self.ratingCompletionForAutoAsk]; - - status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; - } - - status[keyForAppVersion] = @(sessionCountSoFar); - - [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; -} - -- (UIView *)starView -{ - UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; - vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; - - for (int i = 0; i < 5; i++) - { - btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; - btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; - [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; - [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; - [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; - - [vw_star addSubview:btn_star[i]]; - } - - return vw_star; -} - -- (void)setMessage:(NSString *)message -{ - if (!message) - return; - - _message = message; -} - -- (void)onClick_star:(id)sender -{ - UIColor* color = [self activeStarColor]; - NSInteger rating = 0; - - for (int i = 0; i < 5; i++) - { - [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; - - if (btn_star[i] == sender) - { - color = [self passiveStarColor]; - rating = i + 1; - } - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ - { - [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; - }); -} - -- (void)finishWithRating:(NSInteger)rating -{ - if (self.ratingCompletion) - self.ratingCompletion(rating); - - if (rating != 0) - { - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; - } - - self.alertController = nil; - self.ratingCompletion = nil; -} - -- (UIColor *)activeStarColor -{ - return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; -} - -- (UIColor *)passiveStarColor -{ - return [UIColor colorWithWhite:178/255.0 alpha:1]; -} - -#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) - -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); - return; - } + self = [super init]; - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - if (!widgetID.length) - return; - - NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary* widgetInfo = nil; - - if (!error) - { - widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - NSMutableDictionary* userInfo = widgetInfo.mutableCopy; - - if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; - } - else if (![self isDeviceTargetedByWidget:widgetInfo]) - { - userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(error); - }); - return; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; - }); - }]; - - [task resume]; + return self; } -- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +- (void)enterContentZone:(NSArray *)tags { - __block CLYInternalViewController* webVC = CLYInternalViewController.new; - webVC.view.backgroundColor = UIColor.whiteColor; - webVC.view.bounds = UIScreen.mainScreen.bounds; - webVC.modalPresentationStyle = UIModalPresentationCustom; - - WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; - webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [webVC.view addSubview:webView]; - NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; - [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; - - CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; - dismissButton.onClick = ^(id sender) - { - [webVC dismissViewControllerAnimated:YES completion:^ - { - if (completionHandler) - completionHandler(nil); - - webVC = nil; - }]; - }; - [webVC.view addSubview:dismissButton]; - [dismissButton positionToTopRightConsideringStatusBar]; - - [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; + [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; } -- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", - kCountlyEndpointO, - kCountlyEndpointFeedback, - kCountlyEndpointWidget]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; - return request; - } +- (void)presentNPS { + [self presentNPS:nil widgetCallback:nil]; } -- (NSURL *)widgetDisplayURL:(NSString *)widgetID -{ - NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", - kCountlyFBKeyWidgetID, widgetID, - kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", - CountlyConnectionManager.sharedInstance.host, - kCountlyEndpointFeedback, - queryString]; - - return [NSURL URLWithString:URLString]; +- (void)presentNPS:(NSString *)nameIDorTag { + [self presentNPS:nameIDorTag widgetCallback:nil]; } -- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo -{ - BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; - BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; - BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; - BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; - - return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; } -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted -{ - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (!widgetID.length) - return; - - NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; - segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; - segmentation[kCountlyFBKeyRating] = @(rating); - segmentation[kCountlyFBKeyWidgetID] = widgetID; - segmentation[kCountlyFBKeyEmail] = email; - segmentation[kCountlyFBKeyComment] = comment; - segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; +- (void)presentSurvey { + [self presentSurvey:nil widgetCallback:nil]; } - -#pragma mark - Feedbacks (Surveys, NPS) - -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler -{ - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (!CountlyConsentManager.sharedInstance.consentForFeedback) - return; - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; - - NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - NSDictionary *feedbacksResponse = nil; - - if (!error) - { - feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - } - - if (!error) - { - if (((NSHTTPURLResponse*)response).statusCode != 200) - { - NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; - userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; - } - } - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler(nil, error); - }); - - return; - } - - NSMutableArray* feedbacks = NSMutableArray.new; - NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; - for (NSDictionary * feedbackDict in rawFeedbackObjects) - { - CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; - if (feedback) - [feedbacks addObject:feedback]; - } - - dispatch_async(dispatch_get_main_queue(), ^ - { - if (completionHandler) - completionHandler([NSArray arrayWithArray:feedbacks], nil); - }); - }]; - - [task resume]; +- (void)presentSurvey:(NSString *)nameIDorTag { + [self presentSurvey:nameIDorTag widgetCallback:nil]; } -- (NSURLRequest *)feedbacksRequest -{ - NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", - kCountlyQSKeyMethod, kCountlyFBKeyFeedback, - kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, - kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, - kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointO]; - [URL appendString:kCountlyEndpointSDK]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - [URL appendFormat:@"?%@", queryString]; - NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; - } +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [CountlyFeedbacksInternal.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; } -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +- (void)presentRating { + [self presentRating:nil widgetCallback:nil]; } -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +- (void)presentRating:(NSString *)nameIDorTag { + [self presentRating:nameIDorTag widgetCallback:nil]; } - (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); - [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; + [CountlyFeedbacksInternal.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; } - --(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [self getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { - if (error) { - CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); - return; - } - - CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); - - NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; - NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; - - CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); - - CountlyFeedbackWidget *widgetToPresent = nil; - - if (nameIDorTag && nameIDorTag.length > 0) { - for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { - if ([nameIDorTag isEqualToString:feedbackWidget.name] || - [nameIDorTag isEqualToString:feedbackWidget.ID] || - [feedbackWidget.tags containsObject:nameIDorTag]) { - widgetToPresent = feedbackWidget; - CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); - break; - } - } - } - - if (!widgetToPresent && filteredWidgets.count > 0) { - widgetToPresent = filteredWidgets.firstObject; - CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); - } - - if (widgetToPresent) { - [widgetToPresent presentWithCallback:widgetCallback]; - } else { - CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); - } - }]; +- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; } - #endif @end diff --git a/CountlyFeedbacksInternal.h b/CountlyFeedbacksInternal.h new file mode 100644 index 00000000..85518ebe --- /dev/null +++ b/CountlyFeedbacksInternal.h @@ -0,0 +1,41 @@ +// CountlyFeedbacksInternal.h +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import + +@class CountlyFeedbackWidget; + +extern NSString* const kCountlyFBKeyPlatform; +extern NSString* const kCountlyFBKeyAppVersion; +extern NSString* const kCountlyFBKeyWidgetID; +extern NSString* const kCountlyFBKeyID; + +extern NSString* const kCountlyReservedEventStarRating; + +@interface CountlyFeedbacksInternal : NSObject +#if (TARGET_OS_IOS) ++ (instancetype)sharedInstance; + +- (void)showDialog:(void(^)(NSInteger rating))completion; +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler; +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; +- (void)checkForStarRatingAutoAsk; + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) wigetCallback; + +@property (nonatomic) NSString* message; +@property (nonatomic) NSString* dismissButtonTitle; +@property (nonatomic) NSUInteger sessionCount; +@property (nonatomic) BOOL disableAskingForEachAppVersion; +@property (nonatomic, copy) void (^ratingCompletionForAutoAsk)(NSInteger); +#endif +@end diff --git a/CountlyFeedbacksInternal.m b/CountlyFeedbacksInternal.m new file mode 100644 index 00000000..4a7bb9e2 --- /dev/null +++ b/CountlyFeedbacksInternal.m @@ -0,0 +1,568 @@ +// CountlyFeedbacks.m +// +// This code is provided under the MIT License. +// +// Please visit www.count.ly for more information. + +#import "CountlyCommon.h" +#if (TARGET_OS_IOS) +#import +#endif + +@interface CountlyFeedbackWidget () ++ (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; +@end + + + +@interface CountlyFeedbacksInternal () +#if (TARGET_OS_IOS) +@property (nonatomic) UIAlertController* alertController; +@property (nonatomic, copy) void (^ratingCompletion)(NSInteger); +#endif +@end + +NSString* const kCountlyReservedEventStarRating = @"[CLY]_star_rating"; +NSString* const kCountlyStarRatingStatusSessionCountKey = @"kCountlyStarRatingStatusSessionCountKey"; +NSString* const kCountlyStarRatingStatusHasEverAskedAutomatically = @"kCountlyStarRatingStatusHasEverAskedAutomatically"; + +NSString* const kCountlyFBKeyPlatform = @"platform"; +NSString* const kCountlyFBKeyAppVersion = @"app_version"; +NSString* const kCountlyFBKeyRating = @"rating"; +NSString* const kCountlyFBKeyWidgetID = @"widget_id"; +NSString* const kCountlyFBKeyID = @"_id"; +NSString* const kCountlyFBKeyTargetDevices = @"target_devices"; +NSString* const kCountlyFBKeyPhone = @"phone"; +NSString* const kCountlyFBKeyTablet = @"tablet"; +NSString* const kCountlyFBKeyFeedback = @"feedback"; +NSString* const kCountlyFBKeyEmail = @"email"; +NSString* const kCountlyFBKeyComment = @"comment"; +NSString* const kCountlyFBKeyContactMe = @"contactMe"; + +const CGFloat kCountlyStarRatingButtonSize = 40.0; + +@implementation CountlyFeedbacksInternal +#if (TARGET_OS_IOS) +{ + UIButton* btn_star[5]; +} + ++ (instancetype)sharedInstance +{ + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + + static CountlyFeedbacksInternal* s_sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; +} + +- (instancetype)init +{ + if (self = [super init]) + { + NSString* langDesignator = [NSLocale.preferredLanguages.firstObject substringToIndex:2]; + + NSDictionary* dictMessage = + @{ + @"en": @"How would you rate the app?", + @"tr": @"Uygulamayı nasıl değerlendirirsiniz?", + @"ja": @"あなたの評価を教えてください。", + @"zh": @"请告诉我你的评价。", + @"ru": @"Как бы вы оценили приложение?", + @"cz": @"Jak hodnotíte aplikaci?", + @"lv": @"Kā Jūs novērtētu šo lietotni?", + @"bn": @"আপনি কিভাবে এই এপ্লিক্যাশনটি মূল্যায়ন করবেন?", + @"hi": @"आप एप्लीकेशन का मूल्यांकन कैसे करेंगे?", + }; + + self.message = dictMessage[langDesignator]; + if (!self.message) + self.message = dictMessage[@"en"]; + } + + return self; +} + +#pragma mark - Star Rating + +- (void)showDialog:(void(^)(NSInteger rating))completion +{ + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + self.ratingCompletion = completion; + + self.alertController = [UIAlertController alertControllerWithTitle:@" " message:self.message preferredStyle:UIAlertControllerStyleAlert]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton]; + dismissButton.onClick = ^(id sender) + { + [self.alertController dismissViewControllerAnimated:YES completion:^ + { + [self finishWithRating:0]; + }]; + }; + [self.alertController.view addSubview:dismissButton]; + [dismissButton positionToTopRight]; + + CLYInternalViewController* cvc = CLYInternalViewController.new; + [cvc setPreferredContentSize:(CGSize){kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize * 1.5}]; + [cvc.view addSubview:[self starView]]; + + @try + { + [self.alertController setValue:cvc forKey:@"contentViewController"]; + } + @catch (NSException* exception) + { + CLY_LOG_W(@"%s, UIAlertController's contentViewController can not be set, got exception %@", __FUNCTION__, exception); + } + + [CountlyCommon.sharedInstance tryPresentingViewController:self.alertController]; +} + +- (void)checkForStarRatingAutoAsk +{ + if (!self.sessionCount) + return; + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + NSMutableDictionary* status = [CountlyPersistency.sharedInstance retrieveStarRatingStatus].mutableCopy; + + if (self.disableAskingForEachAppVersion && status[kCountlyStarRatingStatusHasEverAskedAutomatically]) + return; + + NSString* keyForAppVersion = [kCountlyStarRatingStatusSessionCountKey stringByAppendingString:CountlyDeviceInfo.appVersion]; + NSInteger sessionCountSoFar = [status[keyForAppVersion] integerValue]; + sessionCountSoFar++; + + if (self.sessionCount == sessionCountSoFar) + { + CLY_LOG_D(@"Asking for star-rating as session count reached specified limit %d ...", (int)self.sessionCount); + + [self showDialog:self.ratingCompletionForAutoAsk]; + + status[kCountlyStarRatingStatusHasEverAskedAutomatically] = @YES; + } + + status[keyForAppVersion] = @(sessionCountSoFar); + + [CountlyPersistency.sharedInstance storeStarRatingStatus:status]; +} + +- (UIView *)starView +{ + UIView* vw_star = [UIView.alloc initWithFrame:(CGRect){0, 0, kCountlyStarRatingButtonSize * 5, kCountlyStarRatingButtonSize}]; + vw_star.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + + for (int i = 0; i < 5; i++) + { + btn_star[i] = [UIButton.alloc initWithFrame:(CGRect){i * kCountlyStarRatingButtonSize, 0, kCountlyStarRatingButtonSize, kCountlyStarRatingButtonSize}]; + btn_star[i].titleLabel.font = [UIFont fontWithName:@"Helvetica" size:28]; + [btn_star[i] setTitle:@"★" forState:UIControlStateNormal]; + [btn_star[i] setTitleColor:[self passiveStarColor] forState:UIControlStateNormal]; + [btn_star[i] addTarget:self action:@selector(onClick_star:) forControlEvents:UIControlEventTouchUpInside]; + + [vw_star addSubview:btn_star[i]]; + } + + return vw_star; +} + +- (void)setMessage:(NSString *)message +{ + if (!message) + return; + + _message = message; +} + +- (void)onClick_star:(id)sender +{ + UIColor* color = [self activeStarColor]; + NSInteger rating = 0; + + for (int i = 0; i < 5; i++) + { + [btn_star[i] setTitleColor:color forState:UIControlStateNormal]; + + if (btn_star[i] == sender) + { + color = [self passiveStarColor]; + rating = i + 1; + } + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ + { + [self.alertController dismissViewControllerAnimated:YES completion:^{ [self finishWithRating:rating]; }]; + }); +} + +- (void)finishWithRating:(NSInteger)rating +{ + if (self.ratingCompletion) + self.ratingCompletion(rating); + + if (rating != 0) + { + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; + } + + self.alertController = nil; + self.ratingCompletion = nil; +} + +- (UIColor *)activeStarColor +{ + return [UIColor colorWithRed:253/255.0 green:148/255.0 blue:38/255.0 alpha:1]; +} + +- (UIColor *)passiveStarColor +{ + return [UIColor colorWithWhite:178/255.0 alpha:1]; +} + +#pragma mark - Feedbacks (Ratings) (Legacy Feedback Widget) + +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'presentRatingWidgetWithID' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + if (!widgetID.length) + return; + + NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary* widgetInfo = nil; + + if (!error) + { + widgetInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + NSMutableDictionary* userInfo = widgetInfo.mutableCopy; + + if (![widgetInfo[kCountlyFBKeyID] isEqualToString:widgetID]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ is not available.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotAvailable userInfo:userInfo]; + } + else if (![self isDeviceTargetedByWidget:widgetInfo]) + { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Feedback widget with ID %@ does not include this device in target devices list.", widgetID]; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbackWidgetNotTargetedForDevice userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(error); + }); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + [self presentRatingWidgetInternal:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + }); + }]; + + [task resume]; +} + +- (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSString *)closeButtonText completionHandler:(void (^)(NSError * error))completionHandler +{ + __block CLYInternalViewController* webVC = CLYInternalViewController.new; + webVC.view.backgroundColor = UIColor.whiteColor; + webVC.view.bounds = UIScreen.mainScreen.bounds; + webVC.modalPresentationStyle = UIModalPresentationCustom; + + WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; + webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [webVC.view addSubview:webView]; + NSURL* widgetDisplayURL = [self widgetDisplayURL:widgetID]; + [webView loadRequest:[NSURLRequest requestWithURL:widgetDisplayURL]]; + + CLYButton* dismissButton = [CLYButton dismissAlertButton:closeButtonText]; + dismissButton.onClick = ^(id sender) + { + [webVC dismissViewControllerAnimated:YES completion:^ + { + if (completionHandler) + completionHandler(nil); + + webVC = nil; + }]; + }; + [webVC.view addSubview:dismissButton]; + [dismissButton positionToTopRightConsideringStatusBar]; + + [CountlyCommon.sharedInstance tryPresentingViewController:webVC]; +} + +- (NSURLRequest *)widgetCheckURLRequest:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyFBKeyWidgetID, widgetID]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* serverOutputFeedbackWidgetEndpoint = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@%@%@", + kCountlyEndpointO, + kCountlyEndpointFeedback, + kCountlyEndpointWidget]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverOutputFeedbackWidgetEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + NSString* withQueryString = [serverOutputFeedbackWidgetEndpoint stringByAppendingFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]]; + return request; + } +} + +- (NSURL *)widgetDisplayURL:(NSString *)widgetID +{ + NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@&%@=%@", + kCountlyFBKeyWidgetID, widgetID, + kCountlyFBKeyAppVersion, CountlyDeviceInfo.appVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSString* URLString = [NSString stringWithFormat:@"%@%@?%@", + CountlyConnectionManager.sharedInstance.host, + kCountlyEndpointFeedback, + queryString]; + + return [NSURL URLWithString:URLString]; +} + +- (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo +{ + BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; + BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; + BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; + BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; + + return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); +} + +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted +{ + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (!widgetID.length) + return; + + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + segmentation[kCountlyFBKeyWidgetID] = widgetID; + segmentation[kCountlyFBKeyEmail] = email; + segmentation[kCountlyFBKeyComment] = comment; + segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; +} + + +#pragma mark - Feedbacks (Surveys, NPS) + +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler +{ + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'getFeedbackWidgets' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + NSDictionary *feedbacksResponse = nil; + + if (!error) + { + feedbacksResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + if (!error) + { + if (((NSHTTPURLResponse*)response).statusCode != 200) + { + NSMutableDictionary* userInfo = feedbacksResponse.mutableCopy; + userInfo[NSLocalizedDescriptionKey] = @"Feedbacks general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorFeedbacksGeneralAPIError userInfo:userInfo]; + } + } + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler(nil, error); + }); + + return; + } + + NSMutableArray* feedbacks = NSMutableArray.new; + NSArray* rawFeedbackObjects = feedbacksResponse[@"result"]; + for (NSDictionary * feedbackDict in rawFeedbackObjects) + { + CountlyFeedbackWidget *feedback = [CountlyFeedbackWidget createWithDictionary:feedbackDict]; + if (feedback) + [feedbacks addObject:feedback]; + } + + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completionHandler) + completionHandler([NSArray arrayWithArray:feedbacks], nil); + }); + }]; + + [task resume]; +} + +- (NSURLRequest *)feedbacksRequest +{ + NSString* queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", + kCountlyQSKeyMethod, kCountlyFBKeyFeedback, + kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, + kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, + kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSMutableString* URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; + [URL appendString:kCountlyEndpointO]; + [URL appendString:kCountlyEndpointSDK]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + [URL appendFormat:@"?%@", queryString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; + return request; + } +} + +- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting NPS widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeNPS nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Survey widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeSurvey nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + +- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + CLY_LOG_D(@"Presenting Rating widget with nameIDorTag: %@ and WidgetCallback: %@", nameIDorTag, widgetCallback); + [self presentFeedbackWidget:CLYFeedbackWidgetTypeRating nameIDorTag:nameIDorTag widgetCallback:widgetCallback]; +} + + +-(void)presentFeedbackWidget:(CLYFeedbackWidgetType)widgetType nameIDorTag:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { + [Countly.sharedInstance getFeedbackWidgets:^(NSArray *feedbackWidgets, NSError *error) { + if (error) { + CLY_LOG_D(@"Getting widgets list failed. Error: %@", error); + return; + } + + CLY_LOG_D(@"Successfully retrieved feedback widgets. Total widgets count: %lu", (unsigned long)feedbackWidgets.count); + + NSPredicate *typePredicate = [NSPredicate predicateWithFormat:@"type == %@", widgetType]; + NSArray *filteredWidgets = [feedbackWidgets filteredArrayUsingPredicate:typePredicate]; + + CLY_LOG_D(@"Filtered widgets count for type '%@': %lu", widgetType, (unsigned long)filteredWidgets.count); + + CountlyFeedbackWidget *widgetToPresent = nil; + + if (nameIDorTag && nameIDorTag.length > 0) { + for (CountlyFeedbackWidget *feedbackWidget in filteredWidgets) { + if ([nameIDorTag isEqualToString:feedbackWidget.name] || + [nameIDorTag isEqualToString:feedbackWidget.ID] || + [feedbackWidget.tags containsObject:nameIDorTag]) { + widgetToPresent = feedbackWidget; + CLY_LOG_D(@"Exact match found for nameIDorTag '%@'. Widget ID: %@, Name: %@", nameIDorTag, feedbackWidget.ID, feedbackWidget.name); + break; + } + } + } + + if (!widgetToPresent && filteredWidgets.count > 0) { + widgetToPresent = filteredWidgets.firstObject; + CLY_LOG_D(@"No exact match found for nameIDorTag '%@'. Falling back to the first widget of type '%@'. Widget ID: %@, Name: %@", nameIDorTag, widgetType, widgetToPresent.ID, widgetToPresent.name); + } + + if (widgetToPresent) { + [widgetToPresent presentWithCallback:widgetCallback]; + } else { + CLY_LOG_D(@"No feedback widget found for the specified type: %@", widgetType); + } + }]; +} + +#endif +@end diff --git a/CountlyFeedbacksNew.h b/CountlyFeedbacksNew.h deleted file mode 100644 index 1c253cd9..00000000 --- a/CountlyFeedbacksNew.h +++ /dev/null @@ -1,30 +0,0 @@ -// CountlyFeedbacks.h -// -// This code is provided under the MIT License. -// -// Please visit www.count.ly for more information. -// - -#import -#import "CountlyConfig.h" -#import "CountlyFeedbackWidget.h" - -@interface CountlyFeedbacksNew: NSObject -#if (TARGET_OS_IOS) -+ (instancetype)sharedInstance; - -- (void) presentNPS; -- (void) presentNPS:(NSString *)nameIDorTag; -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; - -- (void) presentSurvey; -- (void) presentSurvey:(NSString *)nameIDorTag; -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; - -- (void) presentRating; -- (void) presentRating:(NSString *)nameIDorTag; -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback; - -- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler; -#endif -@end From e4dd4911e781ea9dd01689edad866f79ed000cc6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 12:34:10 +0500 Subject: [PATCH 111/148] Delete CountlyFeedbacksNew.m --- CountlyFeedbacksNew.m | 78 ------------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 CountlyFeedbacksNew.m diff --git a/CountlyFeedbacksNew.m b/CountlyFeedbacksNew.m deleted file mode 100644 index 7d869ed2..00000000 --- a/CountlyFeedbacksNew.m +++ /dev/null @@ -1,78 +0,0 @@ -// CountlyFeedbacks.m -// -// This code is provided under the MIT License. -// -// Please visit www.count.ly for more information. -// - -#import "CountlyFeedbacksNew.h" -#import "CountlyCommon.h" - -@implementation CountlyFeedbacksNew -#if (TARGET_OS_IOS) -+ (instancetype)sharedInstance -{ - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; - - static CountlyFeedbacksNew* s_sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); - return s_sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - - return self; -} - -- (void)enterContentZone:(NSArray *)tags -{ - [CountlyContentBuilderInternal.sharedInstance enterContentZone:tags]; -} - -- (void)presentNPS { - [self presentNPS:nil widgetCallback:nil]; -} - -- (void)presentNPS:(NSString *)nameIDorTag { - [self presentNPS:nameIDorTag widgetCallback:nil]; -} - -- (void) presentNPS:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacks.sharedInstance presentNPS:nameIDorTag widgetCallback:widgetCallback]; -} - -- (void)presentSurvey { - [self presentSurvey:nil widgetCallback:nil]; -} - -- (void)presentSurvey:(NSString *)nameIDorTag { - [self presentSurvey:nameIDorTag widgetCallback:nil]; -} - -- (void) presentSurvey:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacks.sharedInstance presentSurvey:nameIDorTag widgetCallback:widgetCallback]; -} - -- (void)presentRating { - [self presentRating:nil widgetCallback:nil]; -} - -- (void)presentRating:(NSString *)nameIDorTag { - [self presentRating:nameIDorTag widgetCallback:nil]; -} - -- (void) presentRating:(NSString *)nameIDorTag widgetCallback:(WidgetCallback) widgetCallback { - [CountlyFeedbacks.sharedInstance presentRating:nameIDorTag widgetCallback:widgetCallback]; -} - -- (void)getAvailableFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler -{ - CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); - [CountlyFeedbacks.sharedInstance getFeedbackWidgets:completionHandler]; -} -#endif -@end From 8800e207aaff913c9809e22c920c64d5df739eb9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 12:43:01 +0500 Subject: [PATCH 112/148] Symlink added for CountlyFeedbacks.h --- include/CountlyFeedbacks.h | 1 + 1 file changed, 1 insertion(+) create mode 120000 include/CountlyFeedbacks.h diff --git a/include/CountlyFeedbacks.h b/include/CountlyFeedbacks.h new file mode 120000 index 00000000..b7e5847b --- /dev/null +++ b/include/CountlyFeedbacks.h @@ -0,0 +1 @@ +../CountlyFeedbacks.h \ No newline at end of file From 7f0174c9abfa9f43e153db58d2c04582871f79f4 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 12:57:05 +0500 Subject: [PATCH 113/148] Updated changelog --- CHANGELOG.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5403ce31..2db1d3af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## x.x.x -* Added `CountlyFeedbacks` interface to manage feedback widgets, including NPS, Surveys, and Ratings. - * `presentNPS`, `presentSurvey`, and `presentRating` methods now allow displaying specific widgets by name, ID, or tag. - * Optional widget callback support for handling custom logic after displaying feedback widgets. - * Added `getAvailableFeedbackWidgets` method to retrieve available feedback widgets with a completion handler. +* Added `CountlyFeedbacks:` interface with new view methods (Access with `Countly.sharedInstance.feedback`): + * Method to present feedback widget (wih an optional widget selector(name, ID or tag) string and a Callback): + * `presentNPS` + * `presentSurvey` + * `presentRating` + * `getAvailableFeedbackWidgets` method to retrieve available feedback widgets with a completion handler. + +* Deprecated `getFeedbackWidgets` method, you should use `[feedback getAvailableFeedbackWidgets:]` method instead ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) From 88119d04623eda74a9445e432cac07e0149ac020 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 15:30:11 +0500 Subject: [PATCH 114/148] added vision OS for github build --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c5daf8f..45a0561a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,14 +25,17 @@ jobs: {os_version: 14, sdk: "iphoneos17.5"}, {os_version: 14, sdk: "appletvos17.5"}, {os_version: 14, sdk: "watchos10.5"}, + {os_version: 14, sdk: "visionos1.2"}, {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, - {os_version: 12, sdk: "appletvos16.1"}, + {os_version: 12, sdk: "appletvos16.1"},, + {os_version: 12, sdk: "visionos1.0"} {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, - {os_version: 13, sdk: "watchos10.2"} + {os_version: 13, sdk: "watchos10.2"}, + {os_version: 13, sdk: "visionos1.1"} ] steps: # To check the github context From 1beeeb729f75d55e5807045681be4dba0c5e2b3c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 15:41:13 +0500 Subject: [PATCH 115/148] Removed extra comma --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45a0561a..bddb847c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: {os_version: 14, sdk: "visionos1.2"}, {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, - {os_version: 12, sdk: "appletvos16.1"},, + {os_version: 12, sdk: "appletvos16.1"}, {os_version: 12, sdk: "visionos1.0"} {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, From 590e96f030692af84428a6d4d19ae2c8df405ee9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 16:35:09 +0500 Subject: [PATCH 116/148] content json url encoding fix for < 17 --- CountlyContentBuilderInternal.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 59589114..ec0bbcb0 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -133,7 +133,7 @@ - (NSURLRequest *)fetchContentsRequest NSString* queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; NSString *resolutionJson = [self resolutionJson]; queryString = [queryString stringByAppendingFormat:@"&%@=%@", - @"resolution", resolutionJson]; + @"resolution", resolutionJson.cly_URLEscaped]; queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; From 3e82746ce04bf535a44379b8a1ee67fa2fa7f03c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 23 Oct 2024 19:54:59 +0500 Subject: [PATCH 117/148] Added missing comma :) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bddb847c..30012a32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, {os_version: 12, sdk: "appletvos16.1"}, - {os_version: 12, sdk: "visionos1.0"} + {os_version: 12, sdk: "visionos1.0"}, {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, From e03e6f31719c640f79a4a81a91b84277256cf8d6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 11:05:46 +0500 Subject: [PATCH 118/148] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9efaa722..814fec2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## 24.7.4 -* Mitigated an issue with the feedback widget that caused it to display a blank white page on devices running iOS versions earlier than 17 +* Mitigated an issue with URL parameter encoding in the feedback widget that resulted in a blank page on devices running iOS versions prior to 17 * Added `CountlyFeedbacks:` interface with new view methods (Access with `Countly.sharedInstance.feedback`): * Method to present feedback widget (wih an optional widget selector(name, ID or tag) string and a Callback): From bd9c0ce84d7af628c1f73b7df52ee140d02dfcc0 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 11:06:12 +0500 Subject: [PATCH 119/148] Updated SDK version to 24.7.4 --- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Countly-PL.podspec b/Countly-PL.podspec index c7186a68..78897d6b 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.3' + s.version = '24.7.4' 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' diff --git a/Countly.podspec b/Countly.podspec index 04cc60a0..e914373a 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.3' + s.version = '24.7.4' 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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d6bc336f..d90ee9c7 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -734,7 +734,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.3; + MARKETING_VERSION = 24.7.4; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -766,7 +766,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.3; + MARKETING_VERSION = 24.7.4; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index f7564a6c..e0394a94 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -29,7 +29,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.3"; +NSString* const kCountlySDKVersion = @"24.7.4"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; From 0199e24e110a995fba1cca5748c51432eb7059da Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 11:34:19 +0500 Subject: [PATCH 120/148] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 814fec2c..c63fb84d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 24.7.4 -* Mitigated an issue with URL parameter encoding in the feedback widget that resulted in a blank page on devices running iOS versions prior to 17 +* Mitigated an issue with the feedback widget URL encoding on iOS 16 and earlier, which prevented the widget from displaying +* Mitigated an issue with content fetch URL encoding on iOS 16 and earlier, which caused the request to fail * Added `CountlyFeedbacks:` interface with new view methods (Access with `Countly.sharedInstance.feedback`): * Method to present feedback widget (wih an optional widget selector(name, ID or tag) string and a Callback): From 7610151483d88471c42593c719a6710169be740e Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 11:59:44 +0500 Subject: [PATCH 121/148] Updated vision os name to "xros" --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30012a32..ee3ab503 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,17 +25,17 @@ jobs: {os_version: 14, sdk: "iphoneos17.5"}, {os_version: 14, sdk: "appletvos17.5"}, {os_version: 14, sdk: "watchos10.5"}, - {os_version: 14, sdk: "visionos1.2"}, + {os_version: 14, sdk: "xros1.2"}, {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, {os_version: 12, sdk: "appletvos16.1"}, - {os_version: 12, sdk: "visionos1.0"}, + {os_version: 12, sdk: "xros1.0"}, {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, - {os_version: 13, sdk: "visionos1.1"} + {os_version: 13, sdk: "xros1.1"} ] steps: # To check the github context From ae22cff9c5ce0722a9358753af3c112dce0f76a6 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 12:04:09 +0500 Subject: [PATCH 122/148] Updated mac to 14 for vision os builds --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee3ab503..469071cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,13 +29,13 @@ jobs: {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, {os_version: 12, sdk: "appletvos16.1"}, - {os_version: 12, sdk: "xros1.0"}, + {os_version: 14, sdk: "xros1.0"}, {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, - {os_version: 13, sdk: "xros1.1"} + {os_version: 14, sdk: "xros1.1"} ] steps: # To check the github context From a62735bb63bdfe7759e83e14668382a268274802 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 12:08:42 +0500 Subject: [PATCH 123/148] Updated vision os builds --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 469071cb..adc73640 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,13 +29,13 @@ jobs: {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, {os_version: 12, sdk: "appletvos16.1"}, - {os_version: 14, sdk: "xros1.0"}, + {os_version: 14, sdk: "xros1.3"}, {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, - {os_version: 14, sdk: "xros1.1"} + {os_version: 15, sdk: "xros1.3"} ] steps: # To check the github context From 58aa0147154ab8d7f2e4696a92ace05befa8781c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 12:15:56 +0500 Subject: [PATCH 124/148] again updated vision os builds --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index adc73640..3dbb4119 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,13 +29,13 @@ jobs: {os_version: 12, sdk: "iphoneos16.2"}, {os_version: 12, sdk: "macosx13.1"}, {os_version: 12, sdk: "appletvos16.1"}, - {os_version: 14, sdk: "xros1.3"}, {os_version: 12, sdk: "watchos9.1"}, {os_version: 13, sdk: "iphoneos17.2"}, {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, - {os_version: 15, sdk: "xros1.3"} + {os_version: 15, sdk: "xros1.2"}, + {os_version: 15, sdk: "xros2.0"} ] steps: # To check the github context From e06765ba7b735efea69b4e6a3dfb1477141dbf85 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 12:23:18 +0500 Subject: [PATCH 125/148] one more vision os update --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dbb4119..94e5c281 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,8 +34,9 @@ jobs: {os_version: 13, sdk: "macosx14.2"}, {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, - {os_version: 15, sdk: "xros1.2"}, - {os_version: 15, sdk: "xros2.0"} + {os_version: 13, sdk: "xros1.0"}, + {os_version: 15, sdk: "xros2.0"}, + {os_version: 15, sdk: "xros2.1"} ] steps: # To check the github context From 591d346d3619b4be638a8496f37ed89bd5f98f00 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 12:49:17 +0500 Subject: [PATCH 126/148] Removed vision os 2.1 --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94e5c281..dc6084aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,7 @@ jobs: {os_version: 13, sdk: "appletvos17.2"}, {os_version: 13, sdk: "watchos10.2"}, {os_version: 13, sdk: "xros1.0"}, - {os_version: 15, sdk: "xros2.0"}, - {os_version: 15, sdk: "xros2.1"} + {os_version: 15, sdk: "xros2.0"} ] steps: # To check the github context From 428ae7bde78017737bc4ae6d18a77defc6de2cfe Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:10:22 +0500 Subject: [PATCH 127/148] Updated changelog log for visionOS support --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c63fb84d..88b3b7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ ## 24.7.4 -* Mitigated an issue with the feedback widget URL encoding on iOS 16 and earlier, which prevented the widget from displaying -* Mitigated an issue with content fetch URL encoding on iOS 16 and earlier, which caused the request to fail - +* Added visionOS build support * Added `CountlyFeedbacks:` interface with new view methods (Access with `Countly.sharedInstance.feedback`): * Method to present feedback widget (wih an optional widget selector(name, ID or tag) string and a Callback): * `presentNPS` * `presentSurvey` * `presentRating` * `getAvailableFeedbackWidgets` method to retrieve available feedback widgets with a completion handler. + +* Mitigated an issue with the feedback widget URL encoding on iOS 16 and earlier, which prevented the widget from displaying +* Mitigated an issue with content fetch URL encoding on iOS 16 and earlier, which caused the request to fail * Deprecated `getFeedbackWidgets` method, you should use `[feedback getAvailableFeedbackWidgets:]` method instead From f899f17f453047dd0d1d71caa17e6e4a95973401 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:16:29 +0500 Subject: [PATCH 128/148] added build_test.yml --- .github/workflows/build_test.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/build_test.yml diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml new file mode 100644 index 00000000..db773e14 --- /dev/null +++ b/.github/workflows/build_test.yml @@ -0,0 +1,29 @@ +name: XCTest + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test-ios: + runs-on: macos-latest + + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Install any dependencies + - name: Install Dependencies + run: swift package resolve + + # Run tests on iPhone 14 Simulator with iOS 16.0 + - name: Run Tests on iPhone 14 Simulator (iOS 16.0) + run: | + xcodebuild test \ + -scheme 'Requestify' \ + -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' \ No newline at end of file From e3c9bc7b1220bf4b1e5be8cf9f8a14e28f312df0 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:17:23 +0500 Subject: [PATCH 129/148] Updated build_test.yml --- .github/workflows/build_test.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index db773e14..2369c173 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -1,12 +1,14 @@ name: XCTest on: - push: - branches: - - main - pull_request: - branches: - - main + push: + branches: + - master + - staging + pull_request: + branches: + - master + - staging jobs: test-ios: From bc8210c987b73f6821780b81b52cd72437c55352 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:20:52 +0500 Subject: [PATCH 130/148] Updated scheme name --- .github/workflows/build_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 2369c173..38195976 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -27,5 +27,5 @@ jobs: - name: Run Tests on iPhone 14 Simulator (iOS 16.0) run: | xcodebuild test \ - -scheme 'Requestify' \ + -scheme 'CountlyTests' \ -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' \ No newline at end of file From f093814bf0a484e93fc42188cfa3b59b0bb69c0c Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:28:16 +0500 Subject: [PATCH 131/148] Updated build test file --- .github/workflows/build_test.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 38195976..53b21934 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -1,5 +1,9 @@ -name: XCTest +name: Build Tests the SDK +permissions: + checks: write + pull-requests: write + on: push: branches: @@ -13,6 +17,8 @@ on: jobs: test-ios: runs-on: macos-latest + strategy: + fail-fast: false steps: # Checkout the repository From 82d97140084dd793da0b28aa9c6a34529d93e0ab Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:33:55 +0500 Subject: [PATCH 132/148] Remove automatic signing for tests --- Countly.xcodeproj/project.pbxproj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index d90ee9c7..a314919b 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -543,14 +543,16 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.countly.CountlyTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; @@ -567,14 +569,16 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.countly.CountlyTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_COMPILATION_MODE = wholemodule; From c4cb050125f48cfaee833996adac82a515915487 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:44:54 +0500 Subject: [PATCH 133/148] Revert "Remove automatic signing for tests" This reverts commit 82d97140084dd793da0b28aa9c6a34529d93e0ab. --- Countly.xcodeproj/project.pbxproj | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index a314919b..d90ee9c7 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -543,16 +543,14 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.countly.CountlyTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; @@ -569,16 +567,14 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.countly.CountlyTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_COMPILATION_MODE = wholemodule; From b670a15d930c3cbacbf50de43f5ca1fe42dc33b0 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 13:45:16 +0500 Subject: [PATCH 134/148] Removed build_test.yml --- .github/workflows/build_test.yml | 37 -------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/build_test.yml diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml deleted file mode 100644 index 53b21934..00000000 --- a/.github/workflows/build_test.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Build Tests the SDK - -permissions: - checks: write - pull-requests: write - -on: - push: - branches: - - master - - staging - pull_request: - branches: - - master - - staging - -jobs: - test-ios: - runs-on: macos-latest - strategy: - fail-fast: false - - steps: - # Checkout the repository - - name: Checkout repository - uses: actions/checkout@v4 - - # Install any dependencies - - name: Install Dependencies - run: swift package resolve - - # Run tests on iPhone 14 Simulator with iOS 16.0 - - name: Run Tests on iPhone 14 Simulator (iOS 16.0) - run: | - xcodebuild test \ - -scheme 'CountlyTests' \ - -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' \ No newline at end of file From 45865223ff7fa70a801a992fea5aed9b02654feb Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 24 Oct 2024 22:05:59 +0500 Subject: [PATCH 135/148] fixed views test to wait expectations by combining them --- CountlyTests/CountlyViewTests.swift | 499 +++++++++++++++++++--------- 1 file changed, 337 insertions(+), 162 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 38dee23b..1a0ac209 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -17,13 +17,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withName: "View1") expectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled within 5 seconds + wait(for: [expectation], timeout: 5.0) + + // Check recorded events after view has been stopped checkRecordedEventsForView(viewName: "View1") } @@ -31,16 +35,22 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view with segmentation let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["key": "value"]) XCTAssertNotNil(viewID, "View should be started successfully with segmentation.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + let expectation = XCTestExpectation(description: "View should be stopped after 4 seconds.") + + // Stop the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") - expectation.fulfill() + expectation.fulfill() // Fulfill expectation once view is stopped } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the stop operation to complete within the timeout + wait(for: [expectation], timeout: 5.0) + + // Check recorded events for the view with segmentation checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } @@ -48,16 +58,23 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") ?? "" - XCTAssertNotNil(viewID, "View should be started successfully.") + guard let viewID = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View should be started successfully, but viewID is nil.") + return + } + + let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + // Stop the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withID: viewID) - expectation.fulfill() + expectation.fulfill() // Fulfill expectation once view is stopped } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled within 5 seconds + wait(for: [expectation], timeout: 5.0) + + // Check recorded events for the view using the viewID checkRecordedEventsForView(withID: viewID) } @@ -65,19 +82,30 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("View1") - let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") - XCTAssertNotNil(viewID1, "View1 should be started successfully.") - XCTAssertNotNil(viewID2, "View2 should be started successfully.") + // Ensure views are started successfully + guard let viewID1 = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } + + guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { + XCTFail("View2 should be started successfully.") + return + } - let expectation = XCTestExpectation(description: "Wait for 5 seconds before stopping the views.") + let expectation = XCTestExpectation(description: "Views should be stopped after 5 seconds.") + + // Stop the views after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withID: viewID2) expectation.fulfill() } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for the stop operation to complete + wait(for: [expectation], timeout: 7.0) // Increased timeout to ensure sufficient time + + // Check recorded events for both views checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(withID: viewID2) } @@ -86,29 +114,48 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID1 = Countly.sharedInstance().views().startView("View1") - let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") + // Start views + guard let viewID1 = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } + + guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { + XCTFail("View2 should be started successfully.") + return + } + XCTAssertNotNil(viewID1, "View1 should be started successfully.") XCTAssertNotNil(viewID2, "View2 should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") + let resumeExpectation = XCTestExpectation(description: "Resume View1 after pausing for 3 seconds.") + let stopExpectation = XCTestExpectation(description: "Stop both views after resuming View1 for 4 seconds.") + + // Pause View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().pauseView(withID: viewID1) - - // Now wait for 3 seconds before resuming the view - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID1) - - // Wait for another 4 seconds before stopping the views - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withID: viewID2) - expectation.fulfill() - } - } + pauseExpectation.fulfill() } - wait(for: [expectation], timeout: 15.0) // Wait for the expectation to be fulfilled + // Resume View1 after an additional 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { + Countly.sharedInstance().views().resumeView(withID: viewID1) + resumeExpectation.fulfill() + } + + // Stop both views after 4 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 11) { + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID2) + stopExpectation.fulfill() + } + + // Wait for expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 15.0) + + // Check recorded events for both views checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(withID: viewID2) } @@ -117,37 +164,52 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") + // Start view and assert it's started successfully + guard let viewID = Countly.sharedInstance().views().startView("View1") else { + XCTFail("View1 should be started successfully.") + return + } XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") + let resumeExpectation = XCTestExpectation(description: "Resume View1 after 3 seconds of pause.") + let stopExpectation = XCTestExpectation(description: "Stop View1 after another 4 seconds of resuming.") + + // Pause the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().pauseView(withID: viewID) - - let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID) - - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() - } - self.wait(for: [stopExpectation], timeout: 5.0) - } - self.wait(for: [resumeExpectation], timeout: 13.0) + pauseExpectation.fulfill() + } + + // Resume the view after 3 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { // 4 + 3 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation.fulfill() } - wait(for: [expectation], timeout: 10.0) + // Stop the view after another 4 seconds of resuming + DispatchQueue.main.asyncAfter(deadline: .now() + 11) { // 4 + 3 + 4 seconds + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() + } + + // Wait for all expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + + // Check recorded events for the view checkRecordedEventsForView(viewName: "View1") } func testStartViewWhileAutoViewTrackingEnabled() throws { let config = createBaseConfig() - config.enableAutomaticViewTracking = true + config.enableAutomaticViewTracking = true // Enable auto view tracking Countly.sharedInstance().start(with: config) + // Start a manual view tracking call let viewID = Countly.sharedInstance().views().startView("View1") + + // Assert that manual view tracking returns nil when auto tracking is enabled XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") } @@ -155,42 +217,63 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) + // Start the auto-stopped view with segmentation + guard let viewID = Countly.sharedInstance().views().startAutoStoppedView("View1", segmentation: ["key": "value"]) else { + XCTFail("Auto-stopped view should be started successfully with segmentation.") + return + } + XCTAssertNotNil(viewID, "Auto-stopped view should be started successfully with segmentation.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Create an expectation for stopping the view after 4 seconds + let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the auto-stopped view.") + + // Stop the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withID: viewID) - expectation.fulfill() + stopExpectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the stop expectation + wait(for: [stopExpectation], timeout: 6.0) // Allow a small buffer beyond the 4-second delay + + // Check the recorded events for the view, including segmentation checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } - + func testStartAutoStoppedViewAndInitiateAnother() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) let viewID1 = Countly.sharedInstance().views().startAutoStoppedView("View1") XCTAssertNotNil(viewID1, "View1 should be started successfully.") + var viewID2 = "" - let expectation = XCTestExpectation(description: "Wait for 4 seconds before starting the second view.") + let startExpectation = XCTestExpectation(description: "Start second view after 4 seconds") + let stopExpectation = XCTestExpectation(description: "Stop both views after 3 seconds") + + // Start second view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") XCTAssertNotNil(viewID2, "View2 should be started successfully.") - let stopExpectation = XCTestExpectation(description: "Wait for 3 seconds before stopping both views.") + // Fulfill startExpectation after starting View2 + startExpectation.fulfill() + + // Stop both views after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withID: viewID1) Countly.sharedInstance().views().stopView(withID: viewID2) + // Fulfill stopExpectation after stopping both views stopExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) + // Wait for both expectations + wait(for: [startExpectation, stopExpectation], timeout: 10.0) + + // Check recorded events after views have been stopped checkRecordedEventsForView(withID: viewID1) checkRecordedEventsForView(withID: viewID2) } @@ -199,46 +282,63 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before pausing the view.") + // Create expectations + let pauseExpectation = XCTestExpectation(description: "Pause the view after 3 seconds") + let resumeExpectation = XCTestExpectation(description: "Resume the view after another 3 seconds") + let stopExpectation = XCTestExpectation(description: "Stop the view after 4 seconds") + + // Pause the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) - - let resumeExpectation = XCTestExpectation(description: "Wait for 3 seconds before resuming the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().resumeView(withID: viewID) - - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() - } - self.wait(for: [stopExpectation], timeout: 5.0) - } - self.wait(for: [resumeExpectation], timeout: 7.0) + pauseExpectation.fulfill() + } + + // Resume the view after another 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation.fulfill() + } + + // Stop the view after 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + Countly.sharedInstance().views().stopView(withName: "View1") + stopExpectation.fulfill() } - wait(for: [expectation], timeout: 10.0) + // Wait for all expectations to be fulfilled + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + + // Check recorded events after stopping the view checkRecordedEventsForView(viewName: "View1") } - func testStopAllViewsWithSpecificSegmentation() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().startView("View2") + // Start multiple views + let viewID1 = Countly.sharedInstance().views().startView("View1") + let viewID2 = Countly.sharedInstance().views().startView("View2") + + XCTAssertNotNil(viewID1, "View1 should be started successfully.") + XCTAssertNotNil(viewID2, "View2 should be started successfully.") + + // Create expectation for stopping all views + let stopAllViewsExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping all views.") DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopAllViews(["key": "value"]) - expectation.fulfill() + stopAllViewsExpectation.fulfill() } - wait(for: [expectation], timeout: 5.0) // Wait for the expectation to be fulfilled + // Wait for the expectation to be fulfilled + wait(for: [stopAllViewsExpectation], timeout: 5.0) + + // Verify that all views have been stopped with the specified segmentation checkAllViewsStoppedWithSegmentation(["key": "value"]) } @@ -246,21 +346,30 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - Countly.sharedInstance().views().startView("View1") + // Start the view and assert it starts successfully + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View1 should be started successfully.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + // Create expectations + let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + + // Add segmentation after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) + addSegmentationExpectation.fulfill() - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Stop the view after another 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") - stopExpectation.fulfill() + stopViewExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) + + // Check if the recorded events include the added segmentation checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) } @@ -268,24 +377,72 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view and assert it starts successfully let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View1 should be started successfully.") + + // Create expectations + let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - let expectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") + // Add segmentation after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) + addSegmentationExpectation.fulfill() - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") + // Stop the view after another 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withID: viewID) - stopExpectation.fulfill() + stopViewExpectation.fulfill() } - self.wait(for: [stopExpectation], timeout: 5.0) } - wait(for: [expectation], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) + + // Check if the recorded events include the added segmentation checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) } + // Refactor remaining tests similarly... + + func testUpdateSegmentationMultipleTimesOnTheSameView() throws { + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + + let viewID = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID, "View should be started successfully.") + + // Create expectations + let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") + let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") + let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") + + // Add first segmentation + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + waitForStart.fulfill() + + // Add second segmentation + DispatchQueue.main.asyncAfter(deadline: .now() + 4) { + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + waitForSecondSegmentation.fulfill() + + // Stop the view + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + Countly.sharedInstance().views().stopView(withName: "View1") + waitForStop.fulfill() + } + } + } + + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForSecondSegmentation, waitForStop], timeout: 12.0) + + // Check recorded events for the view with the last segmentation + checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + } + func testStartViewWithConsentNotGiven() throws { let config = createBaseConfig() config.requiresConsent = true @@ -302,60 +459,82 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view Countly.sharedInstance().views().startView("View1") - let expectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View1.") + // Create expectations for various events + let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") + let startView2Expectation = XCTestExpectation(description: "Expect View2 to start after 3 seconds.") + let stopView2Expectation = XCTestExpectation(description: "Expect View2 to be stopped after 4 seconds.") + + // Stop View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") + stopView1Expectation.fulfill() // Fulfill View1 stop expectation + // Set global view segmentation Countly.sharedInstance().views().setGlobalViewSegmentation(["key": "value"]) - let startExpectation = XCTestExpectation(description: "Wait for 3 seconds before starting View2.") + // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().startView("View2") + startView2Expectation.fulfill() // Fulfill View2 start expectation + + // Update global view segmentation Countly.sharedInstance().views().updateGlobalViewSegmentation(["key": "newValue"]) - let stopExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping View2.") + // Stop View2 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View2") - stopExpectation.fulfill() + stopView2Expectation.fulfill() // Fulfill View2 stop expectation } - self.wait(for: [stopExpectation], timeout: 5.0) } - self.wait(for: [startExpectation], timeout: 7.0) } - wait(for: [expectation], timeout: 10.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [stopView1Expectation, startView2Expectation, stopView2Expectation], timeout: 12.0) + + // Check if the global segmentation has been applied checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } - + func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view Countly.sharedInstance().views().startView("View1") - let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + // Create expectations for various events + let backgroundAppExpectation = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitInBackgroundExpectation = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitAfterForegroundExpectation = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Background the app after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + backgroundAppExpectation.fulfill() // Fulfill background app expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitInBackgroundExpectation.fulfill() // Fulfill wait in background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the view after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") - waitForForeground.fulfill() + waitAfterForegroundExpectation.fulfill() // Fulfill wait after foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [backgroundAppExpectation, waitInBackgroundExpectation, waitAfterForegroundExpectation], timeout: 12.0) + + // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } @@ -363,31 +542,41 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the views Countly.sharedInstance().views().startView("View1") Countly.sharedInstance().views().startAutoStoppedView("View2") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the views after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withName: "View2") - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + + // Check recorded events for the views after transitions checkRecordedEventsForView(viewName: "View1") checkRecordedEventsForView(viewName: "View2") } @@ -396,32 +585,42 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the view Countly.sharedInstance().views().startView("View1") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // Stop the view after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + + // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } - + func testAttemptToStopANonStartedView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -434,64 +633,40 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") } - func testUpdateSegmentationMultipleTimesOnTheSameView() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View should be started successfully.") - - let waitForStart = XCTestExpectation(description: "Wait for 4 seconds before adding segmentation.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) - - let waitForSecondSegmentation = XCTestExpectation(description: "Wait for 4 seconds before adding second segmentation.") - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) - - let waitForStop = XCTestExpectation(description: "Wait for 3 seconds before stopping the view.") - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().stopView(withName: "View1") - waitForStop.fulfill() - } - self.wait(for: [waitForStop], timeout: 5.0) - } - self.wait(for: [waitForSecondSegmentation], timeout: 5.0) - } - - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply - } - func testBackgroundAndForegroundTriggers() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) Countly.sharedInstance().views().startView("View1") + // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") + let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + + // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulating app going to background + // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + waitForStart.fulfill() // Fulfill the start expectation - let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulating app returning to foreground + // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + waitForBackground.fulfill() // Fulfill the background expectation - let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - waitForForeground.fulfill() + waitForForeground.fulfill() // Fulfill the foreground expectation } - self.wait(for: [waitForForeground], timeout: 5.0) } - self.wait(for: [waitForBackground], timeout: 5.0) } - wait(for: [waitForStart], timeout: 6.0) // Wait for the expectation to be fulfilled + // Wait for all expectations to be fulfilled + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) } - // Helper methods to validate results private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view From 38bdb224af10d8978bef234ecf1e487f6d5e3797 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 25 Oct 2024 21:45:55 +0500 Subject: [PATCH 136/148] Validating recorded views and queued request for views --- CountlyTests/CountlyEventStruct.swift | 8 +- CountlyTests/CountlyViewTests.swift | 357 +++++++++++++++++++++----- 2 files changed, 303 insertions(+), 62 deletions(-) diff --git a/CountlyTests/CountlyEventStruct.swift b/CountlyTests/CountlyEventStruct.swift index 15086096..921d89f6 100644 --- a/CountlyTests/CountlyEventStruct.swift +++ b/CountlyTests/CountlyEventStruct.swift @@ -61,9 +61,9 @@ struct AnyCodable: Codable { struct CountlyEventStruct: Codable { let key: String let ID: String - let CVID: String + let CVID: String? let PVID: String? - let PEID: String + let PEID: String? let segmentation: [String: Any]? let count: UInt let sum: Double @@ -81,9 +81,9 @@ struct CountlyEventStruct: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: .key) ID = try container.decode(String.self, forKey: .ID) - CVID = try container.decode(String.self, forKey: .CVID) + CVID = try container.decodeIfPresent(String.self, forKey: .CVID) PVID = try container.decodeIfPresent(String.self, forKey: .PVID) - PEID = try container.decode(String.self, forKey: .PEID) + PEID = try container.decodeIfPresent(String.self, forKey: .PEID) count = try container.decode(UInt.self, forKey: .count) sum = try container.decode(Double.self, forKey: .sum) timestamp = try container.decode(TimeInterval.self, forKey: .timestamp) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 1a0ac209..e4dccec8 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -8,28 +8,45 @@ import XCTest @testable import Countly -class CountlyViewTrackingTests: CountlyBaseTestCase { +class CountlyViewTrackingTests: CountlyViewBaseTest { func testStartAndStopView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) + // Start the first view with "View1" and set an expectation to stop after 3 seconds let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - let expectation = XCTestExpectation(description: "View should be stopped after 3 seconds.") + let expectation = XCTestExpectation(description: "First view should be stopped after 3 seconds.") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().stopView(withName: "View1") expectation.fulfill() } - // Wait for the expectation to be fulfilled within 5 seconds - wait(for: [expectation], timeout: 5.0) + // Start the second view with "View1" and set an expectation to stop after 5 seconds + let viewID1 = Countly.sharedInstance().views().startView("View1") + XCTAssertNotNil(viewID1, "View should be started successfully.") - // Check recorded events after view has been stopped - checkRecordedEventsForView(viewName: "View1") + let expectation1 = XCTestExpectation(description: "Second view should be stopped after 5 seconds.") + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Countly.sharedInstance().views().stopView(withName: "View1") + expectation1.fulfill() + } + + // Wait for both expectations to be fulfilled within 10 seconds + wait(for: [expectation, expectation1], timeout: 10.0) + + // Verify recorded events + let startedEventsCount = ["View1": 2] // Expecting 2 start events for "View1" + let endedEventsDurations = ["View1": [3, 5]] // Expecting 2 stop events with durations 3 and 5 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } + func testStartAndStopViewWithSegmentation() throws { let config = createBaseConfig() @@ -50,8 +67,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the stop operation to complete within the timeout wait(for: [expectation], timeout: 5.0) - // Check recorded events for the view with segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) + // Verify recorded events + let startedEventsCount = ["View1": 1] // Expecting 1 start events for "View1" + let endedEventsDurations = ["View1": [4]] // Expecting 1 stop events with durations 4 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewAndStopViewWithID() throws { @@ -74,8 +95,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the expectation to be fulfilled within 5 seconds wait(for: [expectation], timeout: 5.0) - // Check recorded events for the view using the viewID - checkRecordedEventsForView(withID: viewID) + // Verify recorded events + let startedEventsCount = ["View1": 1] // Expecting 1 start events for "View1" + let endedEventsDurations = ["View1": [3]] // Expecting 1 stop events with durations 3 seconds + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAndStopMultipleViewsIncludingAutoStoppedViews() throws { @@ -88,17 +113,15 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { return } - guard let viewID2 = Countly.sharedInstance().views().startAutoStoppedView("View2") else { - XCTFail("View2 should be started successfully.") - return - } + Countly.sharedInstance().views().startAutoStoppedView("View2") let expectation = XCTestExpectation(description: "Views should be stopped after 5 seconds.") // Stop the views after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withID: viewID2) + Countly.sharedInstance().views().startView("View3") + + Countly.sharedInstance().views().stopView(withID: viewID1) expectation.fulfill() } @@ -106,8 +129,16 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { wait(for: [expectation], timeout: 7.0) // Increased timeout to ensure sufficient time // Check recorded events for both views - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(withID: viewID2) + // Verify recorded events + let startedEventsCount = ["View1": 1, + "View2" : 1, + "View3" : 1] + + let endedEventsDurations = ["View1": [5], + "View2": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testPauseAndResumeViewsForMultipleViews() throws { @@ -145,8 +176,8 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { resumeExpectation.fulfill() } - // Stop both views after 4 more seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 11) { + // Stop both views after 5 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 12) { Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withID: viewID2) stopExpectation.fulfill() @@ -156,8 +187,15 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 15.0) // Check recorded events for both views - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(withID: viewID2) + // Verify recorded events + let startedEventsCount = ["View1": 1, + "View2" : 1] + + let endedEventsDurations = ["View1": [4, 5], + "View2": [12]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testMultiplePauseAndResumeCyclesOnSameView() throws { @@ -174,7 +212,11 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Create expectations let pauseExpectation = XCTestExpectation(description: "Pause View1 after 4 seconds.") let resumeExpectation = XCTestExpectation(description: "Resume View1 after 3 seconds of pause.") - let stopExpectation = XCTestExpectation(description: "Stop View1 after another 4 seconds of resuming.") + + let pauseExpectation1 = XCTestExpectation(description: "Pause View1 after 3 seconds.") + let resumeExpectation1 = XCTestExpectation(description: "Resume View1 after 5 seconds of pause.") + + let stopExpectation = XCTestExpectation(description: "Stop View1 after another 5 seconds of resuming.") // Pause the view after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { @@ -188,17 +230,37 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { resumeExpectation.fulfill() } + // Pause the view after 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + Countly.sharedInstance().views().pauseView(withID: viewID) + pauseExpectation1.fulfill() + } + + // Resume the view after 3 more seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 15) { // 4 + 3 seconds + Countly.sharedInstance().views().resumeView(withID: viewID) + resumeExpectation1.fulfill() + } + // Stop the view after another 4 seconds of resuming - DispatchQueue.main.asyncAfter(deadline: .now() + 11) { // 4 + 3 + 4 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 20) { // 4 + 3 + 4 seconds Countly.sharedInstance().views().stopView(withName: "View1") stopExpectation.fulfill() } + Countly.sharedInstance().views().startView("View1") + Countly.sharedInstance().views().stopView(withName: "View1") + // Wait for all expectations to be fulfilled - wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + wait(for: [pauseExpectation, resumeExpectation,pauseExpectation1, resumeExpectation1, stopExpectation], timeout: 35.0) - // Check recorded events for the view - checkRecordedEventsForView(viewName: "View1") + // Verify recorded events + let startedEventsCount = ["View1": 2] + + let endedEventsDurations = ["View1": [4, 3, 5, 0]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewWhileAutoViewTrackingEnabled() throws { @@ -208,9 +270,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Start a manual view tracking call let viewID = Countly.sharedInstance().views().startView("View1") - + Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().stopView(withID: viewID) // Assert that manual view tracking returns nil when auto tracking is enabled XCTAssertNil(viewID, "Manual view tracking should be ignored when auto view tracking is enabled.") + // Verify recorded events + let startedEventsCount: [String: Int] = [:] + + let endedEventsDurations : [String: [Int]] = [:] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAndStopAutoStoppedViewWithSegmentation() throws { @@ -237,8 +307,12 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for the stop expectation wait(for: [stopExpectation], timeout: 6.0) // Allow a small buffer beyond the 4-second delay - // Check the recorded events for the view, including segmentation - checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) + let startedEventsCount = ["View1": 1] + + let endedEventsDurations = ["View1": [4]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartAutoStoppedViewAndInitiateAnother() throws { @@ -273,9 +347,14 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for both expectations wait(for: [startExpectation, stopExpectation], timeout: 10.0) - // Check recorded events after views have been stopped - checkRecordedEventsForView(withID: viewID1) - checkRecordedEventsForView(withID: viewID2) + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [4], + "View2": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartRegularViewPauseAndResumeMultipleTimesThenStop() throws { @@ -285,35 +364,44 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Start the view let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNotNil(viewID, "View should be started successfully.") - + var viewID2 = ""; // Create expectations let pauseExpectation = XCTestExpectation(description: "Pause the view after 3 seconds") - let resumeExpectation = XCTestExpectation(description: "Resume the view after another 3 seconds") - let stopExpectation = XCTestExpectation(description: "Stop the view after 4 seconds") + let resumeExpectation = XCTestExpectation(description: "Resume the view after another 4 seconds") + let stopExpectation = XCTestExpectation(description: "Stop the view after 5 seconds") // Pause the view after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { Countly.sharedInstance().views().pauseView(withID: viewID) + viewID2 = Countly.sharedInstance().views().startView("View2") pauseExpectation.fulfill() } // Resume the view after another 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { + DispatchQueue.main.asyncAfter(deadline: .now() + 7) { Countly.sharedInstance().views().resumeView(withID: viewID) + Countly.sharedInstance().views().pauseView(withID: viewID2) resumeExpectation.fulfill() } // Stop the view after 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + DispatchQueue.main.asyncAfter(deadline: .now() + 12) { Countly.sharedInstance().views().stopView(withName: "View1") + Countly.sharedInstance().views().resumeView(withID: viewID2) stopExpectation.fulfill() } // Wait for all expectations to be fulfilled - wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 12.0) + wait(for: [pauseExpectation, resumeExpectation, stopExpectation], timeout: 20) - // Check recorded events after stopping the view - checkRecordedEventsForView(viewName: "View1") + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [3, 5], + "View2": [4]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStopAllViewsWithSpecificSegmentation() throws { @@ -336,10 +424,16 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { } // Wait for the expectation to be fulfilled - wait(for: [stopAllViewsExpectation], timeout: 5.0) + wait(for: [stopAllViewsExpectation], timeout: 6.0) + + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [4], + "View2": [4]] - // Verify that all views have been stopped with the specified segmentation - checkAllViewsStoppedWithSegmentation(["key": "value"]) + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { @@ -498,6 +592,9 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { checkGlobalSegmentationApplied(expected: ["key": "newValue"]) } +} + +class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -564,7 +661,7 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { waitForBackground.fulfill() // Fulfill the background expectation // Wait after foreground for 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Stop the views after returning to foreground Countly.sharedInstance().views().stopView(withName: "View1") Countly.sharedInstance().views().stopView(withName: "View2") @@ -574,11 +671,25 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20) - // Check recorded events for the views after transitions - checkRecordedEventsForView(viewName: "View1") - checkRecordedEventsForView(viewName: "View2") + let startedQueuedEventsCount = ["View1": 1, + "View2": 1] + + let endedQueuedEventsDurations = ["View1": [3], + "View2": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1, + "View2": 1] + + let endedEventsDurations = ["View1": [5], + "View2": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testStartViewBackgroundAppResumeViewWhenReturningToForeground() throws { @@ -620,17 +731,17 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Check recorded events for the view after transitions checkRecordedEventsForView(viewName: "View1") } - + func testAttemptToStopANonStartedView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) // Attempt to stop a non-started view -// let beforeEventCount = getRecordedEventCount() + // let beforeEventCount = getRecordedEventCount() Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") -// let afterEventCount = getRecordedEventCount() + // let afterEventCount = getRecordedEventCount() -// XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") } func testBackgroundAndForegroundTriggers() throws { @@ -666,25 +777,155 @@ class CountlyViewTrackingTests: CountlyBaseTestCase { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) } +} +class CountlyViewBaseTest: CountlyBaseTestCase { + // Helper methods to validate results - private func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { + func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view } - private func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { + func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { // Implement your logic to check recorded events for the specified view ID } - private func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { + func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { // Implement your logic to check that all views have been stopped with specific segmentation } - private func checkGlobalSegmentationApplied(expected: [String: String]) { + func checkGlobalSegmentationApplied(expected: [String: String]) { // Implement your logic to verify global segmentation applied correctly } - private func checkNoRecordedEvents() { + func checkNoRecordedEvents() { // Implement logic to verify no recorded events } + + func validateRecordedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { + // Access recorded events + guard let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? [CountlyEvent] else { + fatalError("Failed to get recordedEvents from CountlyPersistency") + } + + // XCTAssertNotEqual(recordedEvents.count, 0, "No recorded events found") + + // Track occurrences for started and ended events + var actualStartedEventsCount: [String: Int] = [:] + var actualEndedEventsDurations: [String: [Int]] = [:] + + // Iterate through recorded events to populate actual counts and durations + for event in recordedEvents { + // Check for start events with "visit": "1" + if event.key == kCountlyReservedEventView + { + if let eventKey = event.segmentation?["name"] as? String { + if let visit = event.segmentation?["visit"], visit as! Int == 1 { + actualStartedEventsCount[eventKey, default: 0] += 1 + } + else{ + actualEndedEventsDurations[eventKey, default: []].append(Int(event.duration)) + } + } + } + } + + // Validate started events count + for (key, expectedCount) in startedEventsCount { + let actualCount = actualStartedEventsCount[key] ?? 0 + XCTAssertEqual(actualCount, expectedCount, "Started events count for key \(key) does not match expected count \(expectedCount)") + } + + // Validate ended events durations + for (key, expectedDurations) in endedEventsDurations { + let actualDurations = actualEndedEventsDurations[key] ?? [] + + // First, ensure the counts match + XCTAssertEqual(actualDurations.count, expectedDurations.count, "Ended events count for key \(key) does not match expected count \(expectedDurations.count)") + + // Create a mutable copy of actualDurations to modify + var mutableActualDurations = actualDurations + + // Check each duration matches + for (index, expectedDuration) in expectedDurations.enumerated() { + // Check if the expected duration exists in the actual durations + XCTAssertTrue(mutableActualDurations.contains(expectedDuration), "Duration at index \(index) for key \(key) does not match expected duration \(expectedDuration)") + + // Remove the expectedDuration from mutableActualDurations + if let foundIndex = mutableActualDurations.firstIndex(of: expectedDuration) { + mutableActualDurations.remove(at: foundIndex) + } + } + + // Optionally, check if all expected durations have been matched + XCTAssertTrue(mutableActualDurations.isEmpty, "Not all actual durations were matched with expected durations for key \(key)") + } + + } + + func validateQueuedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + // Filter out requests containing "events=" + let eventRequests = queuedRequests.filter { $0.contains("events=") } + + // Initialize dictionaries to track actual counts and durations for verification + var actualStartedEventsCount: [String: Int] = [:] + var actualEndedEventsDurations: [String: [Int]] = [:] + + // Loop through each event request to process events + for request in eventRequests { + // Parse the query parameters + let parsedRequest = parseQueryString(request) + + // Check if "events" parameter exists and parse it + if let eventsJSON = parsedRequest["events"] as? String, + let jsonData = eventsJSON.data(using: .utf8) { + do { + // Decode JSON data into an array of events + let events = try JSONDecoder().decode([CountlyEventStruct].self, from: jsonData) + + // Process each event to check if it’s a start or stop event + for event in events { + if event.key == kCountlyReservedEventView { + let eventKey = event.segmentation?["name"] as? String ?? "" + + // Check for start events with "visit": "1" + if let visit = event.segmentation?["visit"] as? Int, visit == 1 { + actualStartedEventsCount[eventKey, default: 0] += 1 + } + // Check for stop events with "dur" for duration + else { + actualEndedEventsDurations[eventKey, default: []].append(Int(event.duration)) + } + } + } + } catch { + print("Failed to decode events JSON: \(error.localizedDescription)") + } + } + } + + // Validate started events count + for (key, expectedCount) in startedEventsCount { + let actualCount = actualStartedEventsCount[key] ?? 0 + XCTAssertEqual(actualCount, expectedCount, "Started events count for key \(key) does not match expected count \(expectedCount)") + } + + // Validate ended events durations + for (key, expectedDurations) in endedEventsDurations { + let actualDurations = actualEndedEventsDurations[key] ?? [] + XCTAssertEqual(actualDurations.count, expectedDurations.count, "Ended events count for key \(key) does not match expected count \(expectedDurations.count)") + + // Check each duration matches + for (index, expectedDuration) in expectedDurations.enumerated() { + XCTAssertEqual(actualDurations[index], expectedDuration, "Duration at index \(index) for key \(key) does not match expected duration \(expectedDuration)") + } + } + } } + + + From 81dd221d15961e9dc6b3ec799884cd1833725bf1 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 28 Oct 2024 21:24:31 +0500 Subject: [PATCH 137/148] View test finalized --- CountlyTests/CountlyViewTests.swift | 306 +++++++++++++++------------- 1 file changed, 165 insertions(+), 141 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index e4dccec8..896758fe 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -73,6 +73,8 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) + + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "visit": 1, "key": "value", "segment": "iOS"]) } func testStartViewAndStopViewWithID() throws { @@ -436,75 +438,11 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } - func testAddSegmentationToAlreadyStartedViewUsingViewName() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the view and assert it starts successfully - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View1 should be started successfully.") - - // Create expectations - let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") - let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - - // Add segmentation after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value"]) - addSegmentationExpectation.fulfill() - - // Stop the view after another 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withName: "View1") - stopViewExpectation.fulfill() - } - } - - // Wait for all expectations to be fulfilled - wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) - - // Check if the recorded events include the added segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value"]) - } - - func testAddSegmentationToAlreadyStartedViewUsingViewID() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the view and assert it starts successfully - let viewID = Countly.sharedInstance().views().startView("View1") - XCTAssertNotNil(viewID, "View1 should be started successfully.") - - // Create expectations - let addSegmentationExpectation = XCTestExpectation(description: "Wait for 3 seconds before adding segmentation.") - let stopViewExpectation = XCTestExpectation(description: "Wait for 4 seconds before stopping the view.") - - // Add segmentation after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().addSegmentationToView(withID: viewID, segmentation: ["key": "value"]) - addSegmentationExpectation.fulfill() - - // Stop the view after another 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().stopView(withID: viewID) - stopViewExpectation.fulfill() - } - } - - // Wait for all expectations to be fulfilled - wait(for: [addSegmentationExpectation, stopViewExpectation], timeout: 8.0) - - // Check if the recorded events include the added segmentation - checkRecordedEventsForView(withID: viewID, segmentation: ["key": "value"]) - } - - // Refactor remaining tests similarly... - func testUpdateSegmentationMultipleTimesOnTheSameView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) - let viewID = Countly.sharedInstance().views().startView("View1") + let viewID = Countly.sharedInstance().views().startView("View1", segmentation: ["startKey": "startValue"]) XCTAssertNotNil(viewID, "View should be started successfully.") // Create expectations @@ -514,12 +452,12 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Add first segmentation DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value1"]) + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key1": "value1"]) waitForStart.fulfill() // Add second segmentation DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key": "value2"]) + Countly.sharedInstance().views().addSegmentationToView(withName: "View1", segmentation: ["key2": "value2"]) waitForSecondSegmentation.fulfill() // Stop the view @@ -533,8 +471,8 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForSecondSegmentation, waitForStop], timeout: 12.0) - // Check recorded events for the view with the last segmentation - checkRecordedEventsForView(viewName: "View1", segmentation: ["key": "value2"]) // Last segmentation should apply + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "visit": 1, "startKey": "startValue", "segment": "iOS"]) + validateRecordedEventSegmentations(forEventID: viewID ?? "", expectedSegmentations: ["name": "View1", "key1": "value1", "key2": "value2", "segment": "iOS"]) } func testStartViewWithConsentNotGiven() throws { @@ -542,11 +480,18 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { config.requiresConsent = true Countly.sharedInstance().start(with: config) + + let beforeEventCount = getRecordedViews().count; + let viewID = Countly.sharedInstance().views().startView("View1") XCTAssertNil(viewID, "Event should not be recorded when consent is not given.") Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events - checkNoRecordedEvents() + + let afterEventCount = getRecordedViews().count + + XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") + } func testSetAndUpdateGlobalViewSegmentationWithViewInteractions() throws { @@ -560,7 +505,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") let startView2Expectation = XCTestExpectation(description: "Expect View2 to start after 3 seconds.") let stopView2Expectation = XCTestExpectation(description: "Expect View2 to be stopped after 4 seconds.") - + var viewID2 = "" // Stop View1 after 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { Countly.sharedInstance().views().stopView(withName: "View1") @@ -571,7 +516,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().startView("View2") + viewID2 = Countly.sharedInstance().views().startView("View2") startView2Expectation.fulfill() // Fulfill View2 start expectation // Update global view segmentation @@ -588,53 +533,13 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [stopView1Expectation, startView2Expectation, stopView2Expectation], timeout: 12.0) - // Check if the global segmentation has been applied - checkGlobalSegmentationApplied(expected: ["key": "newValue"]) + validateRecordedEventSegmentations(forEventID: viewID2, expectedSegmentations: ["visit": 1, "key": "value", "name": "View2", "segment": "iOS"]) + validateRecordedEventSegmentations(forEventID: viewID2, expectedSegmentations: ["key": "newValue", "name": "View2", "segment": "iOS"]) } } class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { - func testAppTransitionsToBackgroundAndForegroundWithActiveViews() throws { - let config = createBaseConfig() - Countly.sharedInstance().start(with: config) - - // Start the first view - Countly.sharedInstance().views().startView("View1") - - // Create expectations for various events - let backgroundAppExpectation = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") - let waitInBackgroundExpectation = XCTestExpectation(description: "Wait for 4 seconds in background.") - let waitAfterForegroundExpectation = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") - - // Background the app after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Simulate app going to background - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - backgroundAppExpectation.fulfill() // Fulfill background app expectation - - // Wait in background for 4 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - // Simulate app returning to foreground - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - waitInBackgroundExpectation.fulfill() // Fulfill wait in background expectation - - // Wait after foreground for 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // Stop the view after returning to foreground - Countly.sharedInstance().views().stopView(withName: "View1") - waitAfterForegroundExpectation.fulfill() // Fulfill wait after foreground expectation - } - } - } - - // Wait for all expectations to be fulfilled - wait(for: [backgroundAppExpectation, waitInBackgroundExpectation, waitAfterForegroundExpectation], timeout: 12.0) - - // Check recorded events for the view after transitions - checkRecordedEventsForView(viewName: "View1") - } - func testStartMultipleViewsMoveAppToBackgroundAndReturnToForeground() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) @@ -705,7 +610,7 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") // Start the timer for moving the app to the background - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Simulate app going to background NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) waitForStart.fulfill() // Fulfill the start expectation @@ -726,10 +631,21 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20.0) + + let startedQueuedEventsCount = ["View1": 1] + + let endedQueuedEventsDurations = ["View1": [5]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1] + + let endedEventsDurations = ["View1": [3]] - // Check recorded events for the view after transitions - checkRecordedEventsForView(viewName: "View1") + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } func testAttemptToStopANonStartedView() throws { @@ -737,11 +653,11 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { Countly.sharedInstance().start(with: config) // Attempt to stop a non-started view - // let beforeEventCount = getRecordedEventCount() + let beforeEventCount = getRecordedViews().count; Countly.sharedInstance().views().stopView(withName: "ViewNotStarted") - // let afterEventCount = getRecordedEventCount() + let afterEventCount = getRecordedViews().count - // XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not change the state.") + XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") } func testBackgroundAndForegroundTriggers() throws { @@ -775,32 +691,27 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { } // Wait for all expectations to be fulfilled - wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 12.0) + wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 15.0) + + let startedQueuedEventsCount = ["View1": 1] + + let endedQueuedEventsDurations = ["View1": [3]] + + // Call validateRecordedEvents to check if the events match expectations + validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) + + let startedEventsCount = ["View1": 1] + + let endedEventsDurations: [String: [Int]] = [:] + + // Call validateRecordedEvents to check if the events match expectations + validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) } } class CountlyViewBaseTest: CountlyBaseTestCase { // Helper methods to validate results - func checkRecordedEventsForView(viewName: String, segmentation: [String: String]? = nil) { - // Implement your logic to check recorded events for the specified view - } - - func checkRecordedEventsForView(withID viewID: String!, segmentation: [String: String]? = nil) { - // Implement your logic to check recorded events for the specified view ID - } - - func checkAllViewsStoppedWithSegmentation(_ segmentation: [String: String]) { - // Implement your logic to check that all views have been stopped with specific segmentation - } - - func checkGlobalSegmentationApplied(expected: [String: String]) { - // Implement your logic to verify global segmentation applied correctly - } - - func checkNoRecordedEvents() { - // Implement logic to verify no recorded events - } func validateRecordedViews(startedEventsCount: [String: Int], endedEventsDurations: [String: [Int]]) { // Access recorded events @@ -925,6 +836,119 @@ class CountlyViewBaseTest: CountlyBaseTestCase { } } } + + func getRecordedViews() -> [CountlyEvent] { + // Access recorded events + guard let recordedEvents = CountlyPersistency.sharedInstance().value(forKey: "recordedEvents") as? [CountlyEvent] else { + fatalError("Failed to get recordedEvents from CountlyPersistency") + } + + // Filter and return events with the key `kCountlyReservedEventView` + return recordedEvents.filter { $0.key == kCountlyReservedEventView } + } + + func getQueuedViews() -> [CountlyEventStruct] { + guard let queuedRequests = CountlyPersistency.sharedInstance().value(forKey: "queuedRequests") as? [String] else { + fatalError("Failed to get queuedRequests from CountlyPersistency") + } + + // Filter out requests containing "events=" + let eventRequests = queuedRequests.filter { $0.contains("events=") } + var queuedViews: [CountlyEventStruct] = [] + + // Process each event request to extract and filter events + for request in eventRequests { + // Parse the query parameters + let parsedRequest = parseQueryString(request) + + // Check if "events" parameter exists and parse it + if let eventsJSON = parsedRequest["events"] as? String, + let jsonData = eventsJSON.data(using: .utf8) { + do { + // Decode JSON data into an array of events + let events = try JSONDecoder().decode([CountlyEventStruct].self, from: jsonData) + + // Filter and add events with the key `kCountlyReservedEventView` + queuedViews.append(contentsOf: events.filter { $0.key == kCountlyReservedEventView }) + } catch { + print("Failed to decode events JSON: \(error.localizedDescription)") + } + } + } + + return queuedViews + } + + + func validateRecordedEventSegmentations(forEventID eventID: String, expectedSegmentations: [String: Any]) { + // Get recorded views filtered by key + let recordedViews = getRecordedViews() + + // Determine if "visit" is specified in expectedSegmentations + let requiresVisit = expectedSegmentations["visit"] as? Int == 1 + + // Filter events based on the presence and value of "visit" + let filteredEvents = recordedViews.filter { event in + event.id == eventID && + (requiresVisit ? (event.segmentation?["visit"] as? Int == 1) : (event.segmentation?["visit"] == nil)) + } + + // Ensure there are events with the specified ID and segmentation criteria + XCTAssertFalse(filteredEvents.isEmpty, "No recorded events found with ID \(eventID) matching expected segmentation criteria") + + // Validate segmentations for each filtered event + for event in filteredEvents { + guard let eventSegmentations = event.segmentation as? [String: Any] else { + XCTFail("Event segmentation is missing or invalid for event with ID \(eventID)") + continue + } + + // Validate each expected segmentation + for (key, expectedValue) in expectedSegmentations { + if let actualValue = eventSegmentations[key] { + XCTAssertEqual("\(actualValue)", "\(expectedValue)", "Segmentation mismatch for key \(key) in recorded event with ID \(eventID): expected \(expectedValue), found \(actualValue)") + } else { + XCTFail("Segmentation key \(key) missing in recorded event with ID \(eventID)") + } + } + } + } + + func validateQueuedEventSegmentations(forEventID eventID: String, expectedSegmentations: [String: Any]) { + // Get queued views filtered by key + let queuedViews = getQueuedViews() + + // Determine if "visit" is specified in expectedSegmentations + let requiresVisit = expectedSegmentations["visit"] as? Int == 1 + + // Filter events based on the presence and value of "visit" + let filteredEvents = queuedViews.filter { event in + event.ID == eventID && + (requiresVisit ? (event.segmentation?["visit"] as? Int == 1) : (event.segmentation?["visit"] == nil)) + } + + // Ensure there are events with the specified ID and segmentation criteria + XCTAssertFalse(filteredEvents.isEmpty, "No queued events found with ID \(eventID) matching expected segmentation criteria") + + // Validate segmentations for each filtered event + for event in filteredEvents { + guard let eventSegmentations = event.segmentation as? [String: Any] else { + XCTFail("Event segmentation is missing or invalid for event with ID \(eventID)") + continue + } + + // Validate each expected segmentation + for (key, expectedValue) in expectedSegmentations { + if let actualValue = eventSegmentations[key] { + XCTAssertEqual("\(actualValue)", "\(expectedValue)", "Segmentation mismatch for key \(key) in queued event with ID \(eventID): expected \(expectedValue), found \(actualValue)") + } else { + XCTFail("Segmentation key \(key) missing in queued event with ID \(eventID)") + } + } + } + } + + } From b1313a2415b0585e32e861a3cbeeebfb90350ebe Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 28 Oct 2024 21:31:59 +0500 Subject: [PATCH 138/148] Added dummyTest --- CountlyTests/CountlyViewTests.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index 896758fe..be0055f8 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -10,6 +10,16 @@ import XCTest class CountlyViewTrackingTests: CountlyViewBaseTest { + // Run this test first if you are facing cache not clear or instances are not reset properly + // This is a dummy test to cover the edge case clear the cache when SDK is not initialized + func testDummy() { + let config = createBaseConfig() + config.requiresConsent = false; + config.manualSessionHandling = true; + Countly.sharedInstance().start(with: config); + Countly.sharedInstance().halt(true) + } + func testStartAndStopView() throws { let config = createBaseConfig() Countly.sharedInstance().start(with: config) From e7c531ea94cc97328c4b7ec3964dc8d8daba85a8 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Tue, 29 Oct 2024 16:21:07 +0500 Subject: [PATCH 139/148] Added additional checks for content after testing --- CountlyWebViewManager.m | 87 ++++++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index ace4249f..258588a0 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -96,16 +96,23 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati if ([url hasPrefix:@"https://countly_action_event"] && [url containsString:@"cly_x_action_event=1"]) { NSDictionary *queryParameters = [self parseQueryString:url]; NSString *action = queryParameters[@"action"]; - - if ([action isEqualToString:@"event"]) { - NSString *eventsJson = queryParameters[@"event"]; - [self recordEventsWithJSONString:eventsJson]; - } else if ([action isEqualToString:@"link"]) { - NSString *link = queryParameters[@"link"]; - [self openExternalLink:link]; - } else if ([action isEqualToString:@"resize_me"]) { - NSString *resize = queryParameters[@"resize_me"]; - [self resizeWebViewWithJSONString:resize]; + if(action) { + if ([action isEqualToString:@"event"]) { + NSString *eventsJson = queryParameters[@"event"]; + if(eventsJson) { + [self recordEventsWithJSONString:eventsJson]; + } + } else if ([action isEqualToString:@"link"]) { + NSString *link = queryParameters[@"link"]; + if(link) { + [self openExternalLink:link]; + } + } else if ([action isEqualToString:@"resize_me"]) { + NSString *resize = queryParameters[@"resize_me"]; + if(resize) { + [self resizeWebViewWithJSONString:resize]; + } + } } if ([queryParameters[@"close"] boolValue]) { @@ -182,12 +189,30 @@ - (NSDictionary *)parseQueryString:(NSString *)url { } - (void)recordEventsWithJSONString:(NSString *)jsonString { - NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; - NSArray *events = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + // Decode the URL-encoded JSON string + NSString *decodedString = [jsonString stringByRemovingPercentEncoding]; + + // Convert the decoded string to NSData + NSData *data = [decodedString dataUsingEncoding:NSUTF8StringEncoding]; + + // Parse the JSON data + NSError *error = nil; + NSArray *events = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + + if (error) { + NSLog(@"Error parsing JSON: %@", error); + } else { + NSLog(@"Parsed JSON: %@", events); + } + for (NSDictionary *event in events) { NSString *key = event[@"key"]; NSDictionary *segmentation = event[@"sg"]; + if(!key) { + CLY_LOG_I(@"Skipping the event due to key is empty or nil"); + continue; + } if(!segmentation) { CLY_LOG_I(@"Skipping the event due to missing segmentation"); continue; @@ -211,22 +236,55 @@ - (void)openExternalLink:(NSString *)urlString { } - (void)resizeWebViewWithJSONString:(NSString *)jsonString { - NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *resizeDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + // Decode the URL-encoded JSON string + NSString *decodedString = [jsonString stringByRemovingPercentEncoding]; + + // Convert the decoded string to NSData + NSData *data = [decodedString dataUsingEncoding:NSUTF8StringEncoding]; + + // Parse the JSON data + NSError *error = nil; + NSDictionary *resizeDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + + if (!resizeDict) { + CLY_LOG_I(@"Resize dictionary should not be empty or nil. Error: %@", error); + return; + } + + // Ensure resizeDict is a dictionary + if (![resizeDict isKindOfClass:[NSDictionary class]]) { + CLY_LOG_I(@"Resize dictionary should be of type NSDictionary"); + return; + } + + // Retrieve portrait and landscape dimensions NSDictionary *portraitDimensions = resizeDict[@"p"]; NSDictionary *landscapeDimensions = resizeDict[@"l"]; + if (!portraitDimensions && !landscapeDimensions) { + CLY_LOG_I(@"Resize dimensions should not be empty or nil"); + return; + } + + // Determine the current orientation UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); + // Select the appropriate dimensions based on orientation NSDictionary *dimensions = isLandscape ? landscapeDimensions : portraitDimensions; + // Get the dimension values + CGFloat x = [dimensions[@"x"] floatValue]; + CGFloat y = [dimensions[@"y"] floatValue]; CGFloat width = [dimensions[@"w"] floatValue]; CGFloat height = [dimensions[@"h"] floatValue]; + // Animate the resizing of the web view [UIView animateWithDuration:0.3 animations:^{ CGRect frame = self.backgroundView.webView.frame; + frame.origin.x = x; + frame.origin.y = y; frame.size.width = width; frame.size.height = height; self.backgroundView.webView.frame = frame; @@ -236,6 +294,7 @@ - (void)resizeWebViewWithJSONString:(NSString *)jsonString { } + - (void)closeWebView { dispatch_async(dispatch_get_main_queue(), ^{ if (self.dismissBlock) { From c2f5e6560e80c6956a03e96b396069ddeb8b476b Mon Sep 17 00:00:00 2001 From: ijunaid Date: Wed, 30 Oct 2024 17:57:54 +0500 Subject: [PATCH 140/148] Updated test for views --- CountlyTests/CountlyViewTests.swift | 56 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/CountlyTests/CountlyViewTests.swift b/CountlyTests/CountlyViewTests.swift index be0055f8..387a6ea4 100644 --- a/CountlyTests/CountlyViewTests.swift +++ b/CountlyTests/CountlyViewTests.swift @@ -348,7 +348,6 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Stop both views after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - Countly.sharedInstance().views().stopView(withID: viewID1) Countly.sharedInstance().views().stopView(withID: viewID2) // Fulfill stopExpectation after stopping both views @@ -446,6 +445,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) + //TODO: check segmentations also } func testUpdateSegmentationMultipleTimesOnTheSameView() throws { @@ -498,6 +498,11 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { Countly.sharedInstance().views().stopView(withName: "View1") // This should also not affect recorded events + let viewID2 = Countly.sharedInstance().views().startView("View2") + Countly.sharedInstance().views().stopView(withID: viewID2) + //TODO: Add all the public methods + + let afterEventCount = getRecordedViews().count XCTAssertEqual(beforeEventCount, afterEventCount, "Stopping a non-started view should not record any new event.") @@ -510,6 +515,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start the first view Countly.sharedInstance().views().startView("View1") + //TODO: validate that view or remove it // Create expectations for various events let stopView1Expectation = XCTestExpectation(description: "Expect View1 to be stopped after 4 seconds.") @@ -527,6 +533,7 @@ class CountlyViewTrackingTests: CountlyViewBaseTest { // Start View2 after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3) { viewID2 = Countly.sharedInstance().views().startView("View2") + //TODO: also start with segmentation to check the precedence of user provided and global segmentation startView2Expectation.fulfill() // Fulfill View2 start expectation // Update global view segmentation @@ -555,13 +562,15 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { Countly.sharedInstance().start(with: config) // Start the views - Countly.sharedInstance().views().startView("View1") - Countly.sharedInstance().views().startAutoStoppedView("View2") + Countly.sharedInstance().views().startView("V1") + Countly.sharedInstance().views().startAutoStoppedView("A1") // Create expectations for various events let waitForStart = XCTestExpectation(description: "Wait for 3 seconds before backgrounding app.") let waitForBackground = XCTestExpectation(description: "Wait for 4 seconds in background.") let waitForForeground = XCTestExpectation(description: "Wait for 3 seconds after foregrounding.") + let waitBGStartView = XCTestExpectation(description: "Wait for 1 seconds after background.") + let waitFGStartView = XCTestExpectation(description: "Wait for 1 seconds after background.") // Start the timer for moving the app to the background DispatchQueue.main.asyncAfter(deadline: .now() + 3) { @@ -569,17 +578,28 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) waitForStart.fulfill() // Fulfill the start expectation + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Countly.sharedInstance().views().startView("BGV1") + Countly.sharedInstance().views().startAutoStoppedView("BGA1") + waitBGStartView.fulfill() // Fulfill the foreground expectation + } + + // Wait in background for 4 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 4) { // Simulate app returning to foreground NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) waitForBackground.fulfill() // Fulfill the background expectation + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Countly.sharedInstance().views().startView("FGV1") + Countly.sharedInstance().views().startAutoStoppedView("FGA1") + waitFGStartView.fulfill() // Fulfill the foreground expectation + } // Wait after foreground for 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // Stop the views after returning to foreground - Countly.sharedInstance().views().stopView(withName: "View1") - Countly.sharedInstance().views().stopView(withName: "View2") + Countly.sharedInstance().views().stopAllViews(nil); waitForForeground.fulfill() // Fulfill the foreground expectation } } @@ -588,20 +608,28 @@ class CountlyViewForegroundBackgroundTests: CountlyViewBaseTest { // Wait for all expectations to be fulfilled wait(for: [waitForStart, waitForBackground, waitForForeground], timeout: 20) - let startedQueuedEventsCount = ["View1": 1, - "View2": 1] + let startedQueuedEventsCount = ["V1": 1, + "A1": 1] - let endedQueuedEventsDurations = ["View1": [3], - "View2": [3]] + let endedQueuedEventsDurations = ["V1": [3], + "A1": [3]] // Call validateRecordedEvents to check if the events match expectations validateQueuedViews(startedEventsCount: startedQueuedEventsCount, endedEventsDurations: endedQueuedEventsDurations) - let startedEventsCount = ["View1": 1, - "View2": 1] - - let endedEventsDurations = ["View1": [5], - "View2": [5]] + let startedEventsCount = ["BGV1": 1, + "BGA1": 1, + "V1": 1, + "A1": 1, + "FGV1": 1, + "FGA1": 1] + + let endedEventsDurations = ["BGA1": [3], + "A1": [1], + "V1": [5], + "BGV1": [8], + "FGV1": [4], + "FGA1": [4]] // Call validateRecordedEvents to check if the events match expectations validateRecordedViews(startedEventsCount: startedEventsCount, endedEventsDurations: endedEventsDurations) From b02bbe9a07eab55e81706b0e574733739de2f4c8 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 12:19:30 +0500 Subject: [PATCH 141/148] Updated changelog for view fixes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f56a0e..a7bf827f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## xx.xx.xx +* Mitigated an issue where pausing a view resulted in a '0' view duration. +* Mitigated an issue where an `autoStoppedView` would stop again upon returning to the foreground, after already being stopped by the SDK when the app went to the background. +* Improved view duration reporting by sending it as an `NSInteger`, eliminating lengthy decimal sequences previously sent as `NSTimeInterval` + ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) * Updated the SDK to ensure compatibility with the latest server response models From f267f7ac7c5d86cc075e83a59097ceb920f7aec9 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 14:52:38 +0500 Subject: [PATCH 142/148] Updated changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bf827f..5fa2791b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## xx.xx.xx * Mitigated an issue where pausing a view resulted in a '0' view duration. -* Mitigated an issue where an `autoStoppedView` would stop again upon returning to the foreground, after already being stopped by the SDK when the app went to the background. -* Improved view duration reporting by sending it as an `NSInteger`, eliminating lengthy decimal sequences previously sent as `NSTimeInterval` +* Mitigated an issue where an internal timer was not reset when going to foreground for `autoStoppedViews` +* Mitigated an issue for `autoStoppedViews` could have not started when multiple views were open at the same time while going to foreground ## 24.7.3 * Added current view names to event segmentation based on the `enablePreviousNameRecording` (Experimental!) From 843200063f1255e0bb74cafe1f5720f5da72ca58 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 14:56:10 +0500 Subject: [PATCH 143/148] Added changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b3b7dc..f974bdee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## xx.xx.xx +* Mitigated an issue with content action json parsing due to json encoding + ## 24.7.4 * Added visionOS build support * Added `CountlyFeedbacks:` interface with new view methods (Access with `Countly.sharedInstance.feedback`): From 0d31b67440231d9f98c536654b001ff13db4a014 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Thu, 31 Oct 2024 16:23:17 +0500 Subject: [PATCH 144/148] Updated SDK version to 24.7.5 --- CHANGELOG.md | 2 +- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea1e292..b563c7b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## xx.xx.xx +## 24.7.5 * Mitigated an issue with content action json parsing due to json encoding * Mitigated an issue where pausing a view resulted in a '0' view duration. * Mitigated an issue where an internal timer was not reset when going to foreground for `autoStoppedViews` diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 78897d6b..0493ba2f 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.4' + s.version = '24.7.5' 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' diff --git a/Countly.podspec b/Countly.podspec index e914373a..7f40f035 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.4' + s.version = '24.7.5' 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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index dc68ff10..ac8886ee 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -738,7 +738,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.4; + MARKETING_VERSION = 24.7.5; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -770,7 +770,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.4; + MARKETING_VERSION = 24.7.5; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index 061b64f3..4bb1761c 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -29,7 +29,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.4"; +NSString* const kCountlySDKVersion = @"24.7.5"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; From 498c18e391837df0cafd253f83dbe5b4d358993f Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 1 Nov 2024 21:25:07 +0500 Subject: [PATCH 145/148] Fixed visibility and previous name segmentation if no segmentation is provided --- Countly.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Countly.m b/Countly.m index 3b61f6e6..7cf1d60f 100644 --- a/Countly.m +++ b/Countly.m @@ -905,6 +905,8 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co BOOL isReservedEvent = [self isReservedEvent:key]; NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; + if(filteredSegmentations == nil) + filteredSegmentations = NSMutableDictionary.new; // If the event is not reserved, assign the previous event ID and Name to the current event's PEID property, or an empty string if previousEventID is nil. Then, update previousEventID to the current event's ID. if (!isReservedEvent) @@ -941,7 +943,8 @@ - (NSDictionary*) processSegmentation:(NSMutableDictionary *) segmentation event if(CountlyCommon.sharedInstance.enableVisibiltyTracking) { segmentation[kCountlyVisibility] = @([self isAppInForeground]); } - return segmentation; + + return segmentation.count == 0 ? nil : segmentation; } - (BOOL)isAppInForeground { From dd5814cfa6d5d130b4d506e33e2eaeb484bd9758 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 1 Nov 2024 21:40:18 +0500 Subject: [PATCH 146/148] Added changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b563c7b8..569c2042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## xx.xx.xx +* Mitigated an issue with experimental visibility tracking and previous name recording, ensuring they’re included when no segmentation is provided in event or view recording. + ## 24.7.5 * Mitigated an issue with content action json parsing due to json encoding * Mitigated an issue where pausing a view resulted in a '0' view duration. From 93a5dcd1ca38ec6dbdbe8dd00733b2791cf20f06 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 1 Nov 2024 21:43:02 +0500 Subject: [PATCH 147/148] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569c2042..7de7e5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## xx.xx.xx -* Mitigated an issue with experimental visibility tracking and previous name recording, ensuring they’re included when no segmentation is provided in event or view recording. +* Mitigated an issue with experimental visibility tracking and previous name recording, ensuring they’re included even when no segmentation is provided in event or view recording. ## 24.7.5 * Mitigated an issue with content action json parsing due to json encoding From ab91bce1e8b77deafed63846a8f1064b86790676 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Mon, 4 Nov 2024 12:38:01 +0500 Subject: [PATCH 148/148] Updated SDK version to 24.7.6 --- CHANGELOG.md | 2 +- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de7e5c7..4b5a63af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## xx.xx.xx +## 24.7.6 * Mitigated an issue with experimental visibility tracking and previous name recording, ensuring they’re included even when no segmentation is provided in event or view recording. ## 24.7.5 diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 0493ba2f..8631363a 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.7.5' + s.version = '24.7.6' 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' diff --git a/Countly.podspec b/Countly.podspec index 7f40f035..fe20e91b 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.7.5' + s.version = '24.7.6' 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' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index ac8886ee..c03ad326 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -738,7 +738,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.5; + MARKETING_VERSION = 24.7.6; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -770,7 +770,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.7.5; + MARKETING_VERSION = 24.7.6; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index 4bb1761c..893b488d 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -29,7 +29,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.7.5"; +NSString* const kCountlySDKVersion = @"24.7.6"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain";