diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a056363..2aa2470d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +### [Version 4.2.1](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/4.2.1) (March 22, 2023) + +#### Added +- Adds a public method `dismissAppInbox` to dismiss App Inbox. +- Adds a public method `enableLocation` for enabling location API in case of SPM. +- Adds a public method `markReadInboxMessagesForIDs` for marking multiple App Inbox messages as read by passing a collection of `messageID`s. +- Fixes a bug where CoreData would crash with threading inconsistency exceptions. +- Fixes a bug where the method `deleteInboxMessagesForIDs` would cause a crash when the message ID was null or invalid. + ### [Version 4.2.0](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/4.2.0) (December 13, 2022) #### Added diff --git a/CleverTap-iOS-SDK.podspec b/CleverTap-iOS-SDK.podspec index b3d176dc..5dadcf72 100644 --- a/CleverTap-iOS-SDK.podspec +++ b/CleverTap-iOS-SDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "CleverTap-iOS-SDK" -s.version = "4.2.0" +s.version = "4.2.1" s.summary = "The CleverTap iOS SDK for App Analytics and Engagement." s.homepage = "https://github.com/CleverTap/clevertap-ios-sdk" s.license = { :type => "MIT" } diff --git a/CleverTapSDK/CTLocationManager.h b/CleverTapSDK/CTLocationManager.h index 4c9b1cc5..3ac160a5 100644 --- a/CleverTapSDK/CTLocationManager.h +++ b/CleverTapSDK/CTLocationManager.h @@ -3,8 +3,6 @@ @interface CTLocationManager : NSObject -#if defined(CLEVERTAP_LOCATION) + (void)getLocationWithSuccess:(void (^)(CLLocationCoordinate2D location))success andError:(void (^)(NSString *reason))error; -#endif @end diff --git a/CleverTapSDK/CTLocationManager.m b/CleverTapSDK/CTLocationManager.m index 5f89a2a3..ef0fcb3e 100644 --- a/CleverTapSDK/CTLocationManager.m +++ b/CleverTapSDK/CTLocationManager.m @@ -13,7 +13,6 @@ @interface CleverTapLocationRequest : NSObject @implementation CleverTapLocationRequest @end -#if defined(CLEVERTAP_LOCATION) NSString *const kLocationTimeoutError = @"Location Request Timed Out: Have You Set NSLocationWhenInUseUsageDescription in Your Info.plist?"; NSString *const kLocationServicesNotEnabled = @"Location Services Not Enabled"; NSString *const kLocationPermissionDenied = @"Location Permission Denied"; @@ -26,11 +25,9 @@ @implementation CleverTapLocationRequest static NSMutableArray *pendingRequests; static NSObject *requestsLockObject; -#endif @implementation CTLocationManager -#if defined(CLEVERTAP_LOCATION) /** NOTE: If NSLocationWhenInUseUsageDescription is not set in the app's Info.plist, calls to the CLLocationManager instance will fail silently. Rely on the location timeout to stop updating and return an error in this case. */ @@ -201,6 +198,5 @@ + (void)stopUpdatingLocation { locationManager.delegate = nil; [self cancelLocationTimeout]; } -#endif @end diff --git a/CleverTapSDK/CleverTap+Inbox.h b/CleverTapSDK/CleverTap+Inbox.h index 10da6a14..9674c1e2 100755 --- a/CleverTapSDK/CleverTap+Inbox.h +++ b/CleverTapSDK/CleverTap+Inbox.h @@ -203,6 +203,15 @@ typedef void (^CleverTapInboxUpdatedBlock)(void); - (void)markReadInboxMessageForID:(NSString * _Nonnull)messageId; +/*! + @method + + @abstract + This method marks the `CleverTapInboxMessage` object as read for given 'Message Ids` as Collection. + */ + +- (void)markReadInboxMessagesForIDs:(NSArray *_Nonnull)messageIds; + /*! @method @@ -242,5 +251,12 @@ typedef void (^CleverTapInboxUpdatedBlock)(void); */ - (void)recordInboxNotificationClickedEventForID:(NSString * _Nonnull)messageId; +/*! + @method + + @abstract + This method dismisses the inbox controller + */ +- (void)dismissAppInbox; @end diff --git a/CleverTapSDK/CleverTap.h b/CleverTapSDK/CleverTap.h index f824a8ed..1f884a8d 100644 --- a/CleverTapSDK/CleverTap.h +++ b/CleverTapSDK/CleverTap.h @@ -354,6 +354,17 @@ typedef NS_ENUM(int, CTSignedCallEvent) { */ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; +/*! + @method + + @abstract + Enables the location API + + @discussion + Call this method (typically once at app launch) to enable the location API. + + */ ++ (void)enableLocation:(BOOL)enabled; /*! @method diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index 370bb1b6..68d1651d 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -77,6 +77,7 @@ static const void *const kQueueKey = &kQueueKey; static const void *const kNotificationQueueKey = &kNotificationQueueKey; +static BOOL isLocationEnabled; static NSRecursiveLock *instanceLock; static const int kMaxBatchSize = 49; @@ -4078,11 +4079,22 @@ - (BOOL)geofenceLocation { return _geofenceLocation; } ++ (void)enableLocation:(BOOL)enabled{ + isLocationEnabled = enabled; +} + + (void)getLocationWithSuccess:(void (^)(CLLocationCoordinate2D location))success andError:(void (^)(NSString *reason))error; { #if defined(CLEVERTAP_LOCATION) [CTLocationManager getLocationWithSuccess:success andError:error]; #else - CleverTapLogStaticInfo(@"To Enable CleverTap Location services/apis please build the SDK with the CLEVERTAP_LOCATION macro"); + if (isLocationEnabled){ + [CTLocationManager getLocationWithSuccess:success andError:error]; + } + else { + NSString *errorMsg = @"To Enable CleverTap Location services/apis please build the SDK with the CLEVERTAP_LOCATION macro or use enableLocation method"; + CleverTapLogStaticDebug(@"%@",errorMsg); + error(errorMsg); + } #endif } @@ -4354,6 +4366,18 @@ - (void)markReadInboxMessageForID:(NSString *)messageId{ [self.inboxController markReadMessageWithId:messageId]; } +- (void)markReadInboxMessagesForIDs:(NSArray *_Nonnull)messageIds{ + if (![self _isInboxInitialized]) { + return; + } + if (messageIds != nil && [messageIds count] > 0) { + [self.inboxController markReadMessagesWithId:messageIds]; + } + else { + CleverTapLogStaticDebug(@"App Inbox Message IDs array is null or empty"); + } +} + - (void)registerInboxUpdatedBlock:(CleverTapInboxUpdatedBlock)block { if (!_inboxUpdateBlocks) { _inboxUpdateBlocks = [NSMutableArray new]; @@ -4372,6 +4396,19 @@ - (CleverTapInboxViewController * _Nullable)newInboxViewControllerWithConfig:(Cl return [[CleverTapInboxViewController alloc] initWithMessages:messages config:config delegate:delegate analyticsDelegate:self]; } +- (void)dismissAppInbox { + [[self class] runSyncMainQueue:^{ + UIApplication *application = [[self class] getSharedApplication]; + UIWindow *window = [[application delegate] window]; + UIViewController *presentedViewcontoller = [[window rootViewController] presentedViewController]; + if ([presentedViewcontoller isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)[[window rootViewController] presentedViewController]; + if ([navigationController.topViewController isKindOfClass:[CleverTapInboxViewController class]]) { + [[window rootViewController] dismissViewControllerAnimated:YES completion:nil]; + } + } + }]; +} #pragma mark Private diff --git a/CleverTapSDK/CleverTapBuildInfo.h b/CleverTapSDK/CleverTapBuildInfo.h index ef2f044d..a4098590 100644 --- a/CleverTapSDK/CleverTapBuildInfo.h +++ b/CleverTapSDK/CleverTapBuildInfo.h @@ -1,3 +1,3 @@ -#define WR_SDK_REVISION @"40200" +#define WR_SDK_REVISION @"40201" diff --git a/CleverTapSDK/Inbox/controllers/CTInboxController.h b/CleverTapSDK/Inbox/controllers/CTInboxController.h index 5c560ba4..c4d8a832 100755 --- a/CleverTapSDK/Inbox/controllers/CTInboxController.h +++ b/CleverTapSDK/Inbox/controllers/CTInboxController.h @@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)deleteMessageWithId:(NSString *)messageId; - (void)deleteMessagesWithId:(NSArray *_Nonnull)messageIds; - (void)markReadMessageWithId:(NSString *)messageId; +- (void)markReadMessagesWithId:(NSArray *_Nonnull)messageIds; @end diff --git a/CleverTapSDK/Inbox/controllers/CTInboxController.m b/CleverTapSDK/Inbox/controllers/CTInboxController.m index aad25225..5c76bfed 100755 --- a/CleverTapSDK/Inbox/controllers/CTInboxController.m +++ b/CleverTapSDK/Inbox/controllers/CTInboxController.m @@ -103,9 +103,19 @@ - (void)deleteMessageWithId:(NSString *)messageId { - (void)deleteMessagesWithId:(NSArray *_Nonnull)messageIds { NSMutableArray *toDeleteInboxMessages = [NSMutableArray new]; for (NSString *ids in messageIds) { - CTMessageMO *msg = [self _messageForId:ids]; - [toDeleteInboxMessages addObject:msg]; + if (ids != nil && ![ids isEqualToString:@""]){ + CTMessageMO *msg = [self _messageForId:ids]; + if (msg) { + [toDeleteInboxMessages addObject:msg]; + } + else { + CleverTapLogStaticDebug(@"Cannot delete App Inbox Message because Message ID %@ is invalid.", ids) + } + } + else { + CleverTapLogStaticDebug(@"Cannot delete App Inbox Message because Message ID is null or not a string."); } + } if ([toDeleteInboxMessages count] > 0) { [self _deleteMessages:toDeleteInboxMessages]; } @@ -122,6 +132,27 @@ - (void)markReadMessageWithId:(NSString *)messageId { }]; } +- (void)markReadMessagesWithId:(NSArray *_Nonnull)messageIds { + [privateContext performBlock:^{ + for (NSString *ids in messageIds) { + if (ids != nil && ![ids isEqualToString:@""]){ + CTMessageMO *message = [self _messageForId:ids]; + if (message) { + [message setValue:@YES forKey:@"isRead"]; + } + else { + CleverTapLogStaticDebug(@"Cannot mark App Inbox Message as read because Message ID %@ is invalid.", ids); + } + } + else { + CleverTapLogStaticDebug(@"Cannot mark App Inbox Message as read because Message ID is null or not a string."); + } + } + [self _save]; + [self notifyUpdate]; + }]; +} + - (NSDictionary *)messageForId:(NSString *)messageId { if (!self.isInitialized) return nil; CTMessageMO *msg = [self _messageForId:messageId]; @@ -150,21 +181,24 @@ - (NSInteger)unreadCount { BOOL hasMessages = ([[self.user.entity propertiesByName] objectForKey:@"messages"] != nil); if (!hasMessages) return nil; - for (CTMessageMO *msg in self.user.messages) { - int ttl = (int)msg.expires; - if (ttl > 0 && now >= ttl) { - CleverTapLogStaticInternal(@"%@: message expires: %@, deleting", self, msg); - [toDelete addObject:msg]; - } else { - [messages addObject:[msg toJSON]]; + [privateContext performBlockAndWait:^{ + for (CTMessageMO *msg in self.user.messages) { + int ttl = (int)msg.expires; + if (ttl > 0 && now >= ttl) { + CleverTapLogStaticInternal(@"%@: message expires: %@, deleting", self, msg); + [toDelete addObject:msg]; + } else { + [messages addObject:[msg toJSON]]; + } } - } - NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; - [messages sortUsingDescriptors:@[sortDescriptor]]; - - if ([toDelete count] > 0) { - [self _deleteMessages:toDelete]; - } + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; + [messages sortUsingDescriptors:@[sortDescriptor]]; + + if ([toDelete count] > 0) { + [self _deleteMessages:toDelete]; + } + + }]; return messages; } @@ -177,22 +211,26 @@ - (NSInteger)unreadCount { BOOL hasMessages = ([[self.user.entity propertiesByName] objectForKey:@"messages"] != nil); if (!hasMessages) return nil; - NSOrderedSet *results = [self.user.messages filteredOrderedSetUsingPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:@"isRead == NO"]]]; - for (CTMessageMO *msg in results) { - int ttl = (int)msg.expires; - if (ttl > 0 && now >= ttl) { - CleverTapLogStaticInternal(@"%@: message expires: %@, deleting", self, msg); - [toDelete addObject:msg]; - } else { - [messages addObject:[msg toJSON]]; + [privateContext performBlockAndWait:^{ + NSOrderedSet *results = [self.user.messages filteredOrderedSetUsingPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:@"isRead == NO"]]]; + for (CTMessageMO *msg in results) { + int ttl = (int)msg.expires; + if (ttl > 0 && now >= ttl) { + CleverTapLogStaticInternal(@"%@: message expires: %@, deleting", self, msg); + [toDelete addObject:msg]; + } else { + [messages addObject:[msg toJSON]]; + } } - } - NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; - [messages sortUsingDescriptors:@[sortDescriptor]]; + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; + [messages sortUsingDescriptors:@[sortDescriptor]]; + + if ([toDelete count] > 0) { + [self _deleteMessages:toDelete]; + } + }]; + - if ([toDelete count] > 0) { - [self _deleteMessages:toDelete]; - } return messages; } @@ -204,8 +242,12 @@ - (CTMessageMO *)_messageForId:(NSString *)messageId { BOOL hasMessages = ([[self.user.entity propertiesByName] objectForKey:@"messages"] != nil); if (!hasMessages) return nil; - - NSOrderedSet *results = [self.user.messages filteredOrderedSetUsingPredicate:[NSPredicate predicateWithFormat:@"id == %@", messageId]]; + __block NSOrderedSet *results; + + [privateContext performBlockAndWait:^{ + results = [self.user.messages filteredOrderedSetUsingPredicate:[NSPredicate predicateWithFormat:@"id == %@", messageId]]; + }]; + BOOL existing = results && [results count] > 0; return existing ? results[0] : nil; } @@ -222,16 +264,21 @@ - (void)_deleteMessages:(NSArray*)messages { // always call from inside privateContext performBlock - (BOOL)_save { - NSError *error = nil; - BOOL res = YES; - res = [privateContext save:&error]; - if (!res) { - CleverTapLogStaticDebug(@"Error saving core data private context: %@\n%@", [error localizedDescription], [error userInfo]); - } - res = [mainContext save:&error]; - if (!res) { - CleverTapLogStaticDebug(@"Error saving core data main context: %@\n%@", [error localizedDescription], [error userInfo]); - } + __block BOOL res = YES; + [privateContext performBlockAndWait:^{ + NSError *error = nil; + res = [privateContext save:&error]; + if (!res) { + CleverTapLogStaticDebug(@"Error saving core data main context: %@\n%@", [error localizedDescription], [error userInfo]); + } + }]; + [mainContext performBlockAndWait:^{ + NSError *error = nil; + res = [mainContext save:&error]; + if (!res) { + CleverTapLogStaticDebug(@"Error saving core data main context: %@\n%@", [error localizedDescription], [error userInfo]); + } + }]; return res; }