diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 3be67c2c..dc87b37e 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -152,6 +152,8 @@ 07D8C08B21DDEC54006F5A1B /* CTCarouselImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = 07D8C08A21DDEC54006F5A1B /* CTCarouselImageView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 07FD65A2223BC26300A845B7 /* CTCoverViewController~iphoneland.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07FD65A1223BC26300A845B7 /* CTCoverViewController~iphoneland.xib */; }; 07FD65A4223BCB8200A845B7 /* CTCoverViewController~ipadland.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07FD65A3223BCB8200A845B7 /* CTCoverViewController~ipadland.xib */; }; + 0B5564562C25946C00B87284 /* CTUserInfoMigratorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */; }; + 0B995A4A2C36AEDC00AF6006 /* CTLocalDataStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */; }; 1F1C18806B7F29B3374F2448 /* libPods-shared-CleverTapSDKTestsApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E303560B5EE1D154C1E3D9EF /* libPods-shared-CleverTapSDKTestsApp.a */; }; 32394C1F29FA251E00956058 /* CTEventBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32394C1E29FA251E00956058 /* CTEventBuilderTest.m */; }; 32394C2129FA264B00956058 /* CTPreferencesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32394C2029FA264B00956058 /* CTPreferencesTest.m */; }; @@ -273,6 +275,10 @@ 4EA64A2E296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; 4EA64A2F296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; 4EAF05022A495DD5009D9D61 /* CleverTapInstanceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E1F154E27691CA0009387AE /* CleverTapInstanceTests.m */; }; + 4EB3638B2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */; }; + 4EB3638C2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */; }; + 4EB3638D2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EB3638E2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4EB4C8BE2AAD91AC00B7F045 /* CTTriggerEvaluator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB4C8BC2AAD91AC00B7F045 /* CTTriggerEvaluator.h */; }; 4EB4C8BF2AAD91AC00B7F045 /* CTTriggerEvaluator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4C8BD2AAD91AC00B7F045 /* CTTriggerEvaluator.m */; }; 4ECD88312ADC8A05003885CE /* CTSessionManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ECD88302ADC8A05003885CE /* CTSessionManagerTests.m */; }; @@ -702,6 +708,8 @@ 07D8C08A21DDEC54006F5A1B /* CTCarouselImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTCarouselImageView.h; sourceTree = ""; }; 07FD65A1223BC26300A845B7 /* CTCoverViewController~iphoneland.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = "CTCoverViewController~iphoneland.xib"; sourceTree = ""; }; 07FD65A3223BCB8200A845B7 /* CTCoverViewController~ipadland.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = "CTCoverViewController~ipadland.xib"; sourceTree = ""; }; + 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTUserInfoMigratorTest.m; sourceTree = ""; }; + 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTLocalDataStoreTests.m; sourceTree = ""; }; 0CA46771B6F202E37DAC9F70 /* Pods-shared-CleverTapSDKTestsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTestsApp.debug.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTestsApp/Pods-shared-CleverTapSDKTestsApp.debug.xcconfig"; sourceTree = ""; }; 129AEC403AFA828F591B756E /* Pods-shared-CleverTapSDKTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTests.release.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTests/Pods-shared-CleverTapSDKTests.release.xcconfig"; sourceTree = ""; }; 32394C1E29FA251E00956058 /* CTEventBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventBuilderTest.m; sourceTree = ""; }; @@ -779,6 +787,8 @@ 4EA64A25296C115E001D9B22 /* CTRequestFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTRequestFactory.m; sourceTree = ""; }; 4EA64A2A296C1190001D9B22 /* CTRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTRequest.h; sourceTree = ""; }; 4EA64A2B296C1190001D9B22 /* CTRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTRequest.m; sourceTree = ""; }; + 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTUserInfoMigrator.m; sourceTree = ""; }; + 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTUserInfoMigrator.h; sourceTree = ""; }; 4EB4C8BC2AAD91AC00B7F045 /* CTTriggerEvaluator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTriggerEvaluator.h; sourceTree = ""; }; 4EB4C8BD2AAD91AC00B7F045 /* CTTriggerEvaluator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTriggerEvaluator.m; sourceTree = ""; }; 4EC2D084278AAD8000F4DE54 /* IdentityManagementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IdentityManagementTests.m; sourceTree = ""; }; @@ -1444,6 +1454,8 @@ 6A59D20E2A3351A800531F9D /* LeanplumCTTest.m */, 4ECD88302ADC8A05003885CE /* CTSessionManagerTests.m */, 6BD851C82B45CD1800FA5298 /* CTMultiDelegateManager+Tests.h */, + 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */, + 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */, ); path = CleverTapSDKTests; sourceTree = ""; @@ -1627,6 +1639,8 @@ D0CACF8D20B8A44C00A02327 /* CTCertificatePinning.m */, D0CACF9420B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h */, D0CACF9520B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.m */, + 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */, + 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */, 071EB4AE217F6427008F0FAB /* CTInAppUtils.h */, 071EB47D217F6427008F0FAB /* CTInAppUtils.m */, 071EB486217F6427008F0FAB /* CTInAppFCManager.h */, @@ -1743,6 +1757,7 @@ 4E49AE54275D24570074A774 /* CTValidationResultStack.h in Headers */, 07BF465D217F7C88002E166D /* CTInAppDisplayViewControllerPrivate.h in Headers */, 6BD334F42AF7FC660099E33E /* CTTriggerRadius.h in Headers */, + 4EB3638E2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */, 4E838C41299F419900ED0875 /* ContentMerger.h in Headers */, D0BD75A82417694F0006EE55 /* CTFeatureFlagsController.h in Headers */, 4E25E3CD278887A80008C888 /* CTLegacyIdentityRepo.h in Headers */, @@ -1825,6 +1840,7 @@ 4E8B816B2AD2B2FD00714BB4 /* CleverTapInternal.h in Headers */, 07B94541219EA34300D4C542 /* CleverTapInboxViewControllerPrivate.h in Headers */, 071EB4EE217F6427008F0FAB /* CTAlertViewController.h in Headers */, + 4EB3638D2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */, 4E838C4629A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */, D0CACF9620B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h in Headers */, 4E41FD92294F46510001FBED /* CTVar-Internal.h in Headers */, @@ -2286,6 +2302,7 @@ 6AA1357C2A2E467800EFF2C1 /* NSDictionary+Extensions.m in Sources */, D014B8EF20E2FAAD001E0780 /* CleverTapUTMDetail.m in Sources */, 4E41FD9D294F46510001FBED /* CTVarCache.m in Sources */, + 4EB3638C2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */, 4E4E17852B50007D009E2F1E /* CTAES.m in Sources */, D014B91D20E2FBD6001E0780 /* CTCertificatePinning.m in Sources */, D014B90920E2FB71001E0780 /* CTLocalDataStore.m in Sources */, @@ -2340,7 +2357,9 @@ 6BEEC2D12AF1A3A900BD4EC5 /* CTClockMock.m in Sources */, 32790959299F4B29001FE140 /* CTDeviceInfoTest.m in Sources */, 4ECD88312ADC8A05003885CE /* CTSessionManagerTests.m in Sources */, + 0B995A4A2C36AEDC00AF6006 /* CTLocalDataStoreTests.m in Sources */, 6A4427C52AA6515A0098866F /* CTTriggersMatcherTest.m in Sources */, + 0B5564562C25946C00B87284 /* CTUserInfoMigratorTest.m in Sources */, 4E2CF1442AC56D8F00441E8B /* CTEncryptionTests.m in Sources */, 32394C2729FA278C00956058 /* CTUriHelperTest.m in Sources */, 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */, @@ -2482,6 +2501,7 @@ D01A0895207EC2D400423D6F /* CleverTapInstanceConfig.m in Sources */, 071EB4C8217F6427008F0FAB /* CTDismissButton.m in Sources */, 4EF0D5472AD84BCA0044C48F /* CTSessionManager.m in Sources */, + 4EB3638B2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */, 4EB4C8BF2AAD91AC00B7F045 /* CTTriggerEvaluator.m in Sources */, 0701E9652372DE9C0034AAC2 /* CleverTapDisplayUnit.m in Sources */, 072F9E3E21B1368000BC6313 /* CTInboxIconMessageCell.m in Sources */, diff --git a/CleverTapSDK/CTBatchSentDelegate.h b/CleverTapSDK/CTBatchSentDelegate.h index b7445a72..d9e5b96f 100644 --- a/CleverTapSDK/CTBatchSentDelegate.h +++ b/CleverTapSDK/CTBatchSentDelegate.h @@ -7,11 +7,12 @@ // #import +#import "CTQueueType.h" @protocol CTBatchSentDelegate @optional -- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success; +- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType; @optional - (void)onAppLaunchedWithSuccess:(BOOL)success; diff --git a/CleverTapSDK/CTConstants.h b/CleverTapSDK/CTConstants.h index d62f3311..42fd561d 100644 --- a/CleverTapSDK/CTConstants.h +++ b/CleverTapSDK/CTConstants.h @@ -50,6 +50,11 @@ extern NSString *const kSessionId; #define CLTAP_SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) #define CLTAP_APP_LAUNCHED_EVENT @"App Launched" #define CLTAP_CHARGED_EVENT @"Charged" +#define CLTAP_PROFILE @"profile" +#define CLTAP_USER_ATTRIBUTE_CHANGE @"_change" +#define CLTAP_KEY_NEW_VALUE @"newValue" +#define CLTAP_KEY_OLD_VALUE @"oldValue" +#define CLTAP_KEY_PROFILE_ATTR_NAME @"profileAttrName" #define CLTAP_EVENT_NAME @"evtName" #define CLTAP_EVENT_DATA @"evtData" #define CLTAP_CHARGED_EVENT_ITEMS @"Items" @@ -75,6 +80,17 @@ extern NSString *const kSessionId; #define CLTAP_NOTIFICATION_CLICKED_TAG @"wzrk_cts" #define CLTAP_NOTIFICATION_TAG @"W$" #define CLTAP_DATE_FORMAT @"yyyyMMdd" +#define CLTAP_DATE_PREFIX @"$D_" + +// profile commands +static NSString *const kCLTAP_COMMAND_SET = @"$set"; +static NSString *const kCLTAP_COMMAND_ADD = @"$add"; +static NSString *const kCLTAP_COMMAND_REMOVE = @"$remove"; +static NSString *const kCLTAP_COMMAND_INCREMENT = @"$incr"; +static NSString *const kCLTAP_COMMAND_DECREMENT = @"$decr"; +static NSString *const kCLTAP_COMMAND_DELETE = @"$delete"; + +#define CLTAP_MULTIVAL_COMMANDS @[kCLTAP_COMMAND_SET, kCLTAP_COMMAND_ADD, kCLTAP_COMMAND_REMOVE] #pragma mark Constants for File Assets #define CLTAP_FILE_URLS_EXPIRY_DICT @"file_urls_expiry_dict" @@ -132,6 +148,8 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_INAPP_SUPPRESSED_META_KEY @"inapps_suppressed" #define CLTAP_INAPP_SS_EVAL_STORAGE_KEY @"inapps_eval" #define CLTAP_INAPP_SUPPRESSED_STORAGE_KEY @"inapps_suppressed" +#define CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE @"inapps_eval_profile" +#define CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE @"inapps_suppressed_profile" #define CLTAP_PREFS_INAPP_SESSION_MAX_KEY @"imc_max" #define CLTAP_PREFS_INAPP_LAST_DATE_KEY @"ict_date" @@ -249,6 +267,7 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #pragma mark Constants for Profile identifier keys #define CLTAP_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email"] // LEGACY KEYS #define CLTAP_ALL_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email", @"Phone"] +#define CLTAP_SKIP_KEYS_USER_ATTRIBUTE_EVALUATION @[@"cc", @"tz", @"Carrier"] #pragma mark Constants for Encryption #define CLTAP_ENCRYPTION_LEVEL @"CleverTapEncryptionLevel" diff --git a/CleverTapSDK/CTLocalDataStore.h b/CleverTapSDK/CTLocalDataStore.h index 7dce5431..2bbd9c02 100644 --- a/CleverTapSDK/CTLocalDataStore.h +++ b/CleverTapSDK/CTLocalDataStore.h @@ -1,12 +1,14 @@ #import #import "CTDeviceInfo.h" +#import "CTDispatchQueueManager.h" @class CleverTapInstanceConfig; @class CleverTapEventDetail; @interface CTLocalDataStore : NSObject -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo; + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo dispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager; - (void)persistEvent:(NSDictionary *)event; @@ -34,6 +36,8 @@ - (id)getProfileFieldForKey:(NSString *)key; +- (NSDictionary *> *)getUserAttributeChangeProperties:(NSDictionary *)event; + - (void)persistLocalProfileIfRequired; - (NSDictionary*)generateBaseProfile; diff --git a/CleverTapSDK/CTLocalDataStore.m b/CleverTapSDK/CTLocalDataStore.m index 15874d99..ffc2d16f 100644 --- a/CleverTapSDK/CTLocalDataStore.m +++ b/CleverTapSDK/CTLocalDataStore.m @@ -11,6 +11,10 @@ #import "CTPreferences.h" #import "CTUtils.h" #import "CTUIUtils.h" +#import "CTUserInfoMigrator.h" +#import "CTDispatchQueueManager.h" +#import "CTMultiDelegateManager.h" +#import "CTProfileBuilder.h" static const void *const kProfileBackgroundQueueKey = &kProfileBackgroundQueueKey; static const double kProfilePersistenceIntervalSeconds = 30.f; @@ -29,15 +33,17 @@ @interface CTLocalDataStore() { @property (nonatomic, strong) CleverTapInstanceConfig *config; @property (nonatomic, strong) CTDeviceInfo *deviceInfo; @property (nonatomic, strong) NSArray *piiKeys; +@property (nonatomic, strong) CTDispatchQueueManager *dispatchQueueManager; @end @implementation CTLocalDataStore -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo { +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo dispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager { if (self = [super init]) { _config = config; _deviceInfo = deviceInfo; + self.dispatchQueueManager = dispatchQueueManager; localProfileUpdateExpiryStore = [NSMutableDictionary new]; _backgroundQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.clevertap.profileBackgroundQueue:%@", _config.accountId] UTF8String], DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_backgroundQueue, kProfileBackgroundQueueKey, (__bridge void *)self, NULL); @@ -45,6 +51,9 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:( _piiKeys = CLTAP_ENCRYPTION_PII_DATA; [self runOnBackgroundQueue:^{ @synchronized (self->localProfileForSession) { + // migrate to new persisted ct-accid-guid-userprofile + [CTUserInfoMigrator migrateUserInfoFileForAccountID:self->_config.accountId deviceID:self->_deviceInfo.deviceId]; + self->localProfileForSession = [self _inflateLocalProfile]; for (NSString* key in [profileValues allKeys]) { [self setProfileFieldWithKey:key andValue:profileValues[key]]; @@ -88,9 +97,12 @@ - (BOOL)inBackgroundQueue { - (void)changeUser { localProfileUpdateExpiryStore = [NSMutableDictionary new]; - localProfileForSession = [NSMutableDictionary dictionary]; - // this will remove the old profile from the file system - [self _persistLocalProfileAsyncWithCompletion:nil]; + localProfileForSession = [NSMutableDictionary new]; + [self runOnBackgroundQueue:^{ + @synchronized (self->localProfileForSession) { + self->localProfileForSession = [self _inflateLocalProfile]; + } + }]; [self clearStoredEvents]; } @@ -511,6 +523,73 @@ - (id)getProfileFieldForKey:(NSString *)key { } } +- (NSDictionary *> *)getUserAttributeChangeProperties:(NSDictionary *)event { + NSMutableDictionary *> *userAttributesChangeProperties = [NSMutableDictionary dictionary]; + NSMutableDictionary *fieldsToPersistLocally = [NSMutableDictionary dictionary]; + NSDictionary *profile = event[CLTAP_PROFILE]; + if (!profile) { + return @{}; + } + for (NSString *key in profile) { + if ([CLTAP_SKIP_KEYS_USER_ATTRIBUTE_EVALUATION containsObject: key]) { + continue; + } + id oldValue = [self getProfileFieldForKey:key]; + id newValue = profile[key]; + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + if ([newValue isKindOfClass:[NSDictionary class]]) { + NSDictionary *obj = (NSDictionary *)newValue; + NSString *commandIdentifier = [[obj allKeys] firstObject]; + id value = [obj objectForKey:commandIdentifier]; + if ([commandIdentifier isEqualToString:kCLTAP_COMMAND_INCREMENT] || + [commandIdentifier isEqualToString:kCLTAP_COMMAND_DECREMENT]) { + newValue = [CTProfileBuilder _getUpdatedValue:value forKey:key withCommand:commandIdentifier cachedValue:oldValue]; + } else if ([commandIdentifier isEqualToString:kCLTAP_COMMAND_DELETE]) { + newValue = nil; + [self removeProfileFieldForKey:key]; + } + } else if ([newValue isKindOfClass:[NSString class]]) { + // Remove the date prefix before evaluation and persisting + NSString *newValueStr = (NSString *)newValue; + if ([newValueStr hasPrefix:CLTAP_DATE_PREFIX]) { + newValue = @([[newValueStr substringFromIndex:[CLTAP_DATE_PREFIX length]] longLongValue]); + } + } + if (oldValue != nil && ![oldValue isKindOfClass:[NSArray class]]) { + [properties setObject:oldValue forKey:CLTAP_KEY_OLD_VALUE]; + } + if (newValue != nil && ![newValue isKindOfClass:[NSArray class]]) { + [properties setObject:newValue forKey:CLTAP_KEY_NEW_VALUE]; + } + + // Skip evaluation if both newValue or oldValue are null + if ([properties count] > 0) { + [userAttributesChangeProperties setObject:properties forKey:key]; + } + // Need to persist only if the new profile value is not a null value + if (newValue != nil && newValue != oldValue) { + [fieldsToPersistLocally setObject:newValue forKey:key]; + } + } + [self updateProfileFieldsLocally:fieldsToPersistLocally]; + return userAttributesChangeProperties; +} + +-(void) updateProfileFieldsLocally: (NSMutableDictionary *) fieldsToPersistLocally{ + [self.dispatchQueueManager runSerialAsync:^{ + [CTProfileBuilder build:fieldsToPersistLocally completionHandler:^(NSDictionary *customFields, NSDictionary *systemFields, NSArray*errors) { + if (systemFields) { + CleverTapLogInternal(self.config.logLevel, @"%@: Constructed system profile: %@", self, systemFields); + [self setProfileFields:systemFields]; + } + if (customFields) { + CleverTapLogInternal(self.config.logLevel, @"%@: Constructed custom profile: %@", self, customFields); + [self setProfileFields:customFields]; + } + }]; + }]; +} + - (void)setProfileFields:(NSDictionary *)fields { [self setProfileFields:fields fromUpstream:NO]; } @@ -615,14 +694,16 @@ - (id)_getProfileFieldFromSessionCacheWithKey:(NSString *)key { id val = nil; @synchronized (localProfileForSession) { - val = localProfileForSession[key]; + // CACHED VALUES HAVE a "user" PREFIX, SO PREPEND IT BEFORE SEARCHING CACHE + NSString *keyToSearch = [localProfileForSession.allKeys containsObject:key] ? key : [NSString stringWithFormat:@"user%@",key]; + val = localProfileForSession[keyToSearch]; } return val; } - (NSString *)profileFileName { - return [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", self.config.accountId]; + return [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", self.config.accountId, _deviceInfo.deviceId]; } - (NSMutableDictionary *)_inflateLocalProfile { diff --git a/CleverTapSDK/CTMultiDelegateManager.h b/CleverTapSDK/CTMultiDelegateManager.h index 5896688b..aa5e60e0 100644 --- a/CleverTapSDK/CTMultiDelegateManager.h +++ b/CleverTapSDK/CTMultiDelegateManager.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)addBatchSentDelegate:(id)delegate; - (void)removeBatchSentDelegate:(id)delegate; -- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success; +- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType; @end diff --git a/CleverTapSDK/CTMultiDelegateManager.m b/CleverTapSDK/CTMultiDelegateManager.m index c9c21a24..0a2e4f02 100644 --- a/CleverTapSDK/CTMultiDelegateManager.m +++ b/CleverTapSDK/CTMultiDelegateManager.m @@ -76,11 +76,11 @@ - (void)removeBatchSentDelegate:(id)delegate { [self.batchSentDelegates removeObject:delegate]; } -- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success { +- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType{ NSNumber *isAppLaunched = nil; for (id batchSentDelegate in self.batchSentDelegates) { - if ([batchSentDelegate respondsToSelector:@selector(onBatchSent: withSuccess:)]) { - [batchSentDelegate onBatchSent:batchWithHeader withSuccess:success]; + if ([batchSentDelegate respondsToSelector:@selector(onBatchSent: withSuccess:withQueueType:)]) { + [batchSentDelegate onBatchSent:batchWithHeader withSuccess:success withQueueType:queueType]; } if ([batchSentDelegate respondsToSelector:@selector(onAppLaunchedWithSuccess:)]) { if (isAppLaunched == nil) { diff --git a/CleverTapSDK/CTProfileBuilder.h b/CleverTapSDK/CTProfileBuilder.h index 89765bb2..b3e93be5 100644 --- a/CleverTapSDK/CTProfileBuilder.h +++ b/CleverTapSDK/CTProfileBuilder.h @@ -23,4 +23,6 @@ + (void)buildDecrementValueBy:(NSNumber *_Nonnull)value forKey: (NSString *_Nonnull)key localDataStore:(CTLocalDataStore* _Nonnull)dataStore completionHandler:(void(^ _Nonnull )(NSDictionary *_Nullable operatorDict, NSNumber *_Nullable updatedValue, NSArray *_Nullable errors))completion; ++ (NSNumber *_Nullable)_getUpdatedValue:(NSNumber *_Nonnull)value forKey:(NSString *_Nonnull)key withCommand:(NSString *_Nonnull)command cachedValue:(id _Nullable)cachedValue; + @end diff --git a/CleverTapSDK/CTProfileBuilder.m b/CleverTapSDK/CTProfileBuilder.m index 65fc3454..49d2a706 100644 --- a/CleverTapSDK/CTProfileBuilder.m +++ b/CleverTapSDK/CTProfileBuilder.m @@ -7,16 +7,6 @@ #import "CTLocalDataStore.h" #import "CTUtils.h" -// profile commands -static NSString *const kCLTAP_COMMAND_SET = @"$set"; -static NSString *const kCLTAP_COMMAND_ADD = @"$add"; -static NSString *const kCLTAP_COMMAND_REMOVE = @"$remove"; -static NSString *const kCLTAP_COMMAND_INCREMENT = @"$incr"; -static NSString *const kCLTAP_COMMAND_DECREMENT = @"$decr"; -static NSString *const kCLTAP_COMMAND_DELETE = @"$delete"; - -#define CLTAP_MULTIVAL_COMMANDS @[kCLTAP_COMMAND_SET, kCLTAP_COMMAND_ADD, kCLTAP_COMMAND_REMOVE] - @implementation CTProfileBuilder + (void)build:(NSDictionary *)profile completionHandler:(void(^ _Nonnull )(NSDictionary* _Nullable customFields, NSDictionary* _Nullable systemFields, NSArray* _Nullable errors))completion { @@ -392,9 +382,18 @@ + (void)_handleIncrementDecrementValue:(NSNumber *_Nonnull)value forKey:(NSStrin NSDictionary* operatorDict = @{ key: @{command: value} }; + id cachedValue = [dataStore getProfileFieldForKey: key]; NSNumber *newValue; + newValue = [self _getUpdatedValue:value forKey:key withCommand:command cachedValue:cachedValue]; - id cachedValue = [dataStore getProfileFieldForKey: key]; + completion(operatorDict, newValue, nil); +} + ++ (NSNumber *_Nullable)_getUpdatedValue:(NSNumber *_Nonnull)value forKey:(NSString *_Nonnull)key withCommand:(NSString *_Nonnull)command cachedValue:(id)cachedValue { + NSDictionary* operatorDict = @{ + key: @{command: value} + }; + NSNumber *newValue; if ([cachedValue isKindOfClass: [NSNumber class]]) { NSNumber *cachedNumber = (NSNumber*)cachedValue; @@ -449,9 +448,8 @@ + (void)_handleIncrementDecrementValue:(NSNumber *_Nonnull)value forKey:(NSStrin break; } } + return newValue; - completion(operatorDict, newValue, nil); } - @end diff --git a/CleverTapSDK/CTUserInfoMigrator.h b/CleverTapSDK/CTUserInfoMigrator.h new file mode 100644 index 00000000..5285a96a --- /dev/null +++ b/CleverTapSDK/CTUserInfoMigrator.h @@ -0,0 +1,14 @@ +// +// CTUserInfoMigrator.h +// Pods +// +// Created by Kushagra Mishra on 29/05/24. +// + +#import + +@interface CTUserInfoMigrator : NSObject + ++ (void)migrateUserInfoFileForAccountID:(NSString *)acc_id deviceID:(NSString *)device_id; + +@end diff --git a/CleverTapSDK/CTUserInfoMigrator.m b/CleverTapSDK/CTUserInfoMigrator.m new file mode 100644 index 00000000..cbc48cdc --- /dev/null +++ b/CleverTapSDK/CTUserInfoMigrator.m @@ -0,0 +1,26 @@ +#import "CTUserInfoMigrator.h" +#import "CTConstants.h" + +@implementation CTUserInfoMigrator + ++ (void)migrateUserInfoFileForAccountID:(NSString *)acc_id deviceID:(NSString *)device_id { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *libraryPath = [paths objectAtIndex:0]; + NSString *userProfileWithAccountID = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *userProfileWithAccountIDPath = [libraryPath stringByAppendingPathComponent:userProfileWithAccountID]; + NSString *userProfileWithAccountIDAndDeviceID = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *userProfileWithAccountIDAndDeviceIDPath = [libraryPath stringByAppendingPathComponent:userProfileWithAccountIDAndDeviceID]; + + NSError *error = nil; + if ([fileManager fileExistsAtPath:userProfileWithAccountIDPath]) { + [fileManager copyItemAtPath:userProfileWithAccountIDPath toPath:userProfileWithAccountIDAndDeviceIDPath error:&error]; + CleverTapLogStaticInternal(@"[CTUserInfo]: Local file copied successfully to %@", userProfileWithAccountIDAndDeviceIDPath); + [fileManager removeItemAtPath:userProfileWithAccountIDPath error:&error]; + return; + } else { + CleverTapLogStaticInternal(@"[CTUserInfo]: Failed to copy local file: %@", error.localizedDescription); + } +} + +@end diff --git a/CleverTapSDK/CTValidator.m b/CleverTapSDK/CTValidator.m index 314ede39..a5329611 100644 --- a/CleverTapSDK/CTValidator.m +++ b/CleverTapSDK/CTValidator.m @@ -185,7 +185,7 @@ + (CTValidationResult *)cleanObjectValue:(NSObject *)o context:(CTValidatorConte } return vr; } else if ([o isKindOfClass:[NSDate class]]) { - NSString *date = [NSString stringWithFormat:@"$D_%d", (int) ((NSDate *) o).timeIntervalSince1970]; + NSString *date = [NSString stringWithFormat:@"%@%d",CLTAP_DATE_PREFIX, (int) ((NSDate *) o).timeIntervalSince1970]; [vr setObject:date]; return vr; diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index 30d2c498..1187bd86 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -460,11 +460,12 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( if (_deviceInfo.timeZone&& ![_deviceInfo.timeZone isEqualToString:@""]) { initialProfileValues[CLTAP_SYS_TZ] = _deviceInfo.timeZone; } - _localDataStore = [[CTLocalDataStore alloc] initWithConfig:_config profileValues:initialProfileValues andDeviceInfo: _deviceInfo]; self.dispatchQueueManager = [[CTDispatchQueueManager alloc]initWithConfig:_config]; self.delegateManager = [[CTMultiDelegateManager alloc] init]; + _localDataStore = [[CTLocalDataStore alloc] initWithConfig:_config profileValues:initialProfileValues andDeviceInfo: _deviceInfo dispatchQueueManager:_dispatchQueueManager]; + _lastAppLaunchedTime = [self eventGetLastTime:@"App Launched"]; self.validationResultStack = [[CTValidationResultStack alloc]initWithConfig: _config]; self.userSetLocation = kCLLocationCoordinate2DInvalid; @@ -1944,7 +1945,7 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp #if !CLEVERTAP_NO_INAPP_SUPPORT // Evaluate the event only if it will be processed [self.dispatchQueueManager runSerialAsync:^{ - [self evaluateOnEvent:event]; + [self evaluateOnEvent:event withType: eventType]; }]; #endif @@ -1959,7 +1960,7 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp } } -- (void)evaluateOnEvent:(NSDictionary *)event { +- (void)evaluateOnEvent:(NSDictionary *)event withType:(CleverTapEventType)eventType { NSString *eventName = event[CLTAP_EVENT_NAME]; // Add the system properties for evaluation NSMutableDictionary *eventData = [[NSMutableDictionary alloc] initWithDictionary:[self generateAppFields]]; @@ -1969,6 +1970,9 @@ - (void)evaluateOnEvent:(NSDictionary *)event { if (eventName && [eventName isEqualToString:CLTAP_CHARGED_EVENT]) { NSArray *items = eventData[CLTAP_CHARGED_EVENT_ITEMS]; [self.inAppEvaluationManager evaluateOnChargedEvent:eventData andItems:items]; + } else if (eventType == CleverTapEventTypeProfile) { + NSDictionary *> *result = [self.localDataStore getUserAttributeChangeProperties:event]; + [self.inAppEvaluationManager evaluateOnUserAttributeChange:result]; } else if (eventName) { [self.inAppEvaluationManager evaluateOnEvent:eventName withProps:eventData]; } @@ -2209,7 +2213,7 @@ - (void)sendQueue:(NSMutableArray *)queue ofType:(CTQueueType)queueType { [self scheduleQueueFlush]; [self handleSendQueueFail]; - [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:NO]; + [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:NO withQueueType:queueType]; } if (!success || redirect) { @@ -2222,7 +2226,7 @@ - (void)sendQueue:(NSMutableArray *)queue ofType:(CTQueueType)queueType { [self parseResponse:responseData]; - [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:YES]; + [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:YES withQueueType:queueType]; CleverTapLogDebug(self.config.logLevel,@"%@: Successfully sent %lu events", self, (unsigned long)[batch count]); @@ -2557,9 +2561,6 @@ - (void) _asyncSwitchUser:(NSDictionary *)properties withCachedGuid:(NSString *) // clear ARP and other context for the old user [self clearUserContext]; - // clear old profile data - [self.localDataStore changeUser]; - [self.sessionManager resetSession]; if (cachedGUID) { @@ -2570,6 +2571,9 @@ - (void) _asyncSwitchUser:(NSDictionary *)properties withCachedGuid:(NSString *) [self.deviceInfo forceNewDeviceID]; } + // clear old profile data + [self.localDataStore changeUser]; + [self recordDeviceErrors]; [[self delegateManager] notifyDelegatesDeviceIdDidChange:self.deviceInfo.deviceId]; @@ -2726,17 +2730,14 @@ - (void)onUserLogin:(NSDictionary *_Nonnull)properties withCleverTapID:(NSString - (void)profilePush:(NSDictionary *)properties { [self.dispatchQueueManager runSerialAsync:^{ [CTProfileBuilder build:properties completionHandler:^(NSDictionary *customFields, NSDictionary *systemFields, NSArray*errors) { + NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; if (systemFields) { - [self.localDataStore setProfileFields:systemFields]; + [profile addEntriesFromDictionary:systemFields]; } - NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; if (customFields) { - CleverTapLogInternal(self.config.logLevel, @"%@: Constructed custom profile: %@", self, customFields); - [self.localDataStore setProfileFields:customFields]; [profile addEntriesFromDictionary:customFields]; } [self cacheGUIDSforProfile:profile]; - #if !defined(CLEVERTAP_TVOS) // make sure Phone is a string and debug check for country code and phone format, but always send NSArray *profileAllKeys = [profile allKeys]; @@ -2776,6 +2777,10 @@ - (NSString *)profileGetCleverTapID { return self.deviceInfo.deviceId; } +- (id)profileGetLocalValues:(NSString *)propertyName { + return [self.localDataStore getProfileFieldForKey:propertyName]; +} + - (NSString *)getAccountID { return self.config.accountId; } @@ -2802,7 +2807,6 @@ - (void)profileRemoveValueForKey:(NSString *)key { NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; NSString* _key = [customFields allKeys][0]; CleverTapLogInternal(self.config.logLevel, @"%@: removing key %@ from profile", self, _key); - [self.localDataStore removeProfileFieldForKey:_key]; [profile addEntriesFromDictionary:customFields]; NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; @@ -2893,8 +2897,6 @@ - (void)_handleIncrementDecrementProfilePushForKey:(NSString *)key updatedValue: [profile addEntriesFromDictionary:operatorDict]; CleverTapLogInternal(self.config.logLevel, @"Created Increment/ Decrement profile push: %@", operatorDict); - [self.localDataStore setProfileFieldWithKey: key andValue: updatedValue]; - NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; event[@"profile"] = profile; [self queueEvent:event withType:CleverTapEventTypeProfile]; @@ -2922,7 +2924,6 @@ - (void)_handleMultiValueProfilePush:(NSDictionary*)customFields updatedMultiVal } } - #pragma mark - User Action Events API - (void)recordEvent:(NSString *)event { diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.h b/CleverTapSDK/InApps/CTInAppEvaluationManager.h index 4b06163d..2dbea791 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.h +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.h @@ -34,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)evaluateOnEvent:(NSString *)eventName withProps:(NSDictionary *)properties; - (void)evaluateOnChargedEvent:(NSDictionary *)chargeDetails andItems:(NSArray *)items; +- (void)evaluateOnUserAttributeChange:(NSDictionary *)properties; - (void)evaluateOnAppLaunchedClientSide; - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs; diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.m b/CleverTapSDK/InApps/CTInAppEvaluationManager.m index 175ed4e8..088d7e91 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.m +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.m @@ -23,6 +23,8 @@ @interface CTInAppEvaluationManager() @property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIds; @property (nonatomic, strong) NSMutableArray *suppressedClientSideInApps; +@property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIdsForProfile; +@property (nonatomic, strong) NSMutableArray *suppressedClientSideInAppsForProfile; @property BOOL hasAppLaunchedFailed; @property (nonatomic, strong) NSDictionary *appLaunchedProperties; @@ -36,8 +38,8 @@ @interface CTInAppEvaluationManager() @property (nonatomic, strong) NSString *accountId; @property (nonatomic, strong) NSString *deviceId; -- (void)evaluateServerSide:(CTEventAdapter *)event; -- (void)evaluateClientSide:(CTEventAdapter *)event; +- (void)evaluateServerSide:(NSArray *)events withQueueType:(CTQueueType)queueType; +- (void)evaluateClientSide:(NSArray *)events; - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApps; @end @@ -68,6 +70,18 @@ - (instancetype)initWithAccountId:(NSString *)accountId if (savedSuppressedClientSideInApps) { self.suppressedClientSideInApps = [savedSuppressedClientSideInApps mutableCopy]; } + + self.evaluatedServerSideInAppIdsForProfile = [NSMutableArray new]; + NSArray *savedEvaluatedServerSideInAppIdsForProfile = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE]]; + if (savedEvaluatedServerSideInAppIdsForProfile) { + self.evaluatedServerSideInAppIdsForProfile = [savedEvaluatedServerSideInAppIdsForProfile mutableCopy]; + } + + self.suppressedClientSideInAppsForProfile = [NSMutableArray new]; + NSArray *savedSuppressedClientSideInAppsForProfile = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE]]; + if (savedSuppressedClientSideInAppsForProfile) { + self.suppressedClientSideInAppsForProfile = [savedSuppressedClientSideInAppsForProfile mutableCopy]; + } self.inAppStore = inAppStore; self.triggersMatcher = [CTTriggersMatcher new]; @@ -87,19 +101,38 @@ - (void)evaluateOnEvent:(NSString *)eventName withProps:(NSDictionary *)properti } CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:eventName eventProperties:properties andLocation:self.location]; - [self evaluateServerSide:event]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + [self evaluateServerSide:eventList withQueueType:CTQueueTypeEvents]; + [self evaluateClientSide:eventList]; +} + +-(void)evaluateOnUserAttributeChange:(NSDictionary *)profile { + NSDictionary *appFields = self.appLaunchedProperties; + NSMutableArray *eventAdapterList = [NSMutableArray array]; + [profile enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + NSString *eventName = [key stringByAppendingString:CLTAP_USER_ATTRIBUTE_CHANGE]; + NSMutableDictionary *eventProperties = [NSMutableDictionary dictionaryWithDictionary:value]; + [eventProperties addEntriesFromDictionary:appFields]; + CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:eventName profileAttrName:key eventProperties: value andLocation:self.location]; + [eventAdapterList addObject:event]; + }]; + [self evaluateServerSide:eventAdapterList withQueueType:CTQueueTypeProfile]; + [self evaluateClientSide:eventAdapterList]; + } - (void)evaluateOnChargedEvent:(NSDictionary *)chargeDetails andItems:(NSArray *)items { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_CHARGED_EVENT eventProperties:chargeDetails location:self.location andItems:items]; - [self evaluateServerSide:event]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + CTQueueType queueType = CTQueueTypeEvents; + [self evaluateServerSide:eventList withQueueType:queueType]; + [self evaluateClientSide:eventList]; } - (void)evaluateOnAppLaunchedClientSide { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_APP_LAUNCHED_EVENT eventProperties:self.appLaunchedProperties andLocation:self.location]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + [self evaluateClientSide:eventList]; } - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs { @@ -116,8 +149,16 @@ - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs { } } -- (void)evaluateClientSide:(CTEventAdapter *)event { - NSMutableArray *eligibleInApps = [self evaluate:event withInApps:self.inAppStore.clientSideInApps]; +- (void)evaluateClientSide:(NSArray *)events { + NSMutableArray *eligibleInApps = [NSMutableArray array]; + for (CTEventAdapter *event in events) { + id oldValue = [event.eventProperties objectForKey:CLTAP_KEY_OLD_VALUE]; + id newValue = [event.eventProperties objectForKey:CLTAP_KEY_NEW_VALUE]; + if (event.profileAttrName != nil && newValue == oldValue) { + continue; + } + [eligibleInApps addObjectsFromArray:[self evaluate:event withInApps:self.inAppStore.clientSideInApps]]; + } [self sortByPriority:eligibleInApps]; for (NSDictionary *inApp in eligibleInApps) { @@ -132,8 +173,11 @@ - (void)evaluateClientSide:(CTEventAdapter *)event { } } -- (void)evaluateServerSide:(CTEventAdapter *)event { - NSArray *eligibleInApps = [self evaluate:event withInApps:self.inAppStore.serverSideInApps]; +- (void)evaluateServerSide:(NSArray *)events withQueueType:(CTQueueType)queueType{ + NSMutableArray *eligibleInApps = [NSMutableArray array]; + for (CTEventAdapter *event in events) { + [eligibleInApps addObjectsFromArray:[self evaluate:event withInApps:self.inAppStore.serverSideInApps]]; + } BOOL updated = NO; for (NSDictionary *inApp in eligibleInApps) { NSString *campaignId = [CTInAppNotification inAppId:inApp]; @@ -141,12 +185,22 @@ - (void)evaluateServerSide:(CTEventAdapter *)event { NSNumber *cid = [CTUtils numberFromString:campaignId]; if (cid) { updated = YES; - [self.evaluatedServerSideInAppIds addObject:cid]; + if (queueType == CTQueueTypeEvents){ + [self.evaluatedServerSideInAppIds addObject:cid]; + } + else if (queueType == CTQueueTypeProfile){ + [self.evaluatedServerSideInAppIdsForProfile addObject:cid]; + } } } } if (updated) { - [self saveEvaluatedServerSideInAppIds]; + if (queueType == CTQueueTypeEvents){ + [self saveEvaluatedServerSideInAppIds]; + } + else if (queueType == CTQueueTypeProfile){ + [self saveEvaluatedServerSideInAppIdsForProfile]; + } } } @@ -181,11 +235,17 @@ - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApp return eligibleInApps; } -- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success { +- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType{ if (success) { NSDictionary *header = batchWithHeader[0]; - [self removeSentEvaluatedServerSideInAppIds:header]; - [self removeSentSuppressedClientSideInApps:header]; + if (queueType == CTQueueTypeEvents) { + [self removeSentEvaluatedServerSideInAppIds:header]; + [self removeSentSuppressedClientSideInApps:header]; + } + else if (queueType == CTQueueTypeProfile) { + [self removeSentEvaluatedServerSideInAppIdsForProfile:header]; + [self removeSentSuppressedClientSideInAppsForProfile:header]; + } } } @@ -215,6 +275,25 @@ - (void)removeSentSuppressedClientSideInApps:(NSDictionary *)header { } } +- (void)removeSentEvaluatedServerSideInAppIdsForProfile:(NSDictionary *)header { + NSArray *inapps_eval = header[CLTAP_INAPP_SS_EVAL_META_KEY]; + if (inapps_eval && [inapps_eval count] > 0) { + NSUInteger len = inapps_eval.count > self.evaluatedServerSideInAppIdsForProfile.count ? self.evaluatedServerSideInAppIdsForProfile.count : inapps_eval.count; + [self.evaluatedServerSideInAppIdsForProfile removeObjectsInRange:NSMakeRange(0, len)]; + [self saveEvaluatedServerSideInAppIdsForProfile]; + } +} + +- (void)removeSentSuppressedClientSideInAppsForProfile:(NSDictionary *)header { + NSArray *suppresed_inapps = header[CLTAP_INAPP_SUPPRESSED_META_KEY]; + if (suppresed_inapps && [suppresed_inapps count] > 0) { + NSUInteger len = suppresed_inapps.count > self.suppressedClientSideInAppsForProfile.count ? self.suppressedClientSideInAppsForProfile.count : suppresed_inapps.count; + [self.suppressedClientSideInAppsForProfile removeObjectsInRange:NSMakeRange(0, len)]; + [self saveSuppressedClientSideInAppsForProfile]; + } +} + + - (BOOL)shouldSuppress:(NSDictionary *)inApp { return [inApp[CLTAP_INAPP_IS_SUPPRESSED] boolValue]; } @@ -298,18 +377,26 @@ - (void)updateTTL:(NSMutableDictionary *)inApp { - (BatchHeaderKeyPathValues)onBatchHeaderCreationForQueue:(CTQueueType)queueType { // Evaluation is done for events only at the moment, // send the evaluated and suppressed ids in that queue header - if (queueType != CTQueueTypeEvents) { + if (queueType != CTQueueTypeEvents && queueType != CTQueueTypeProfile) { return [NSMutableDictionary new]; } - NSMutableDictionary *header = [NSMutableDictionary new]; - if ([self.evaluatedServerSideInAppIds count] > 0) { - header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIds; + if (queueType == CTQueueTypeProfile) { + if ([self.evaluatedServerSideInAppIdsForProfile count] > 0) { + header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIdsForProfile; + } + if ([self.suppressedClientSideInAppsForProfile count] > 0) { + header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInAppsForProfile; + } } - if ([self.suppressedClientSideInApps count] > 0) { - header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInApps; + else { + if ([self.evaluatedServerSideInAppIds count] > 0) { + header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIds; + } + if ([self.suppressedClientSideInApps count] > 0) { + header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInApps; + } } - return header; } @@ -321,6 +408,14 @@ - (void)saveSuppressedClientSideInApps { [CTPreferences putObject:self.suppressedClientSideInApps forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY]]; } +- (void)saveEvaluatedServerSideInAppIdsForProfile { + [CTPreferences putObject:self.evaluatedServerSideInAppIdsForProfile forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE]]; +} + +- (void)saveSuppressedClientSideInAppsForProfile { + [CTPreferences putObject:self.suppressedClientSideInAppsForProfile forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE]]; +} + - (NSString *)storageKeyWithSuffix:(NSString *)suffix { return [NSString stringWithFormat:@"%@:%@:%@", self.accountId, suffix, self.deviceId]; } diff --git a/CleverTapSDK/InApps/Matchers/CTEventAdapter.h b/CleverTapSDK/InApps/Matchers/CTEventAdapter.h index dca2635c..7bc79c75 100644 --- a/CleverTapSDK/InApps/Matchers/CTEventAdapter.h +++ b/CleverTapSDK/InApps/Matchers/CTEventAdapter.h @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) NSString *eventName; @property (nonatomic, assign, readonly) CLLocationCoordinate2D location; +@property (nonatomic, strong, readonly) NSString *profileAttrName; +@property (nonatomic, strong, nonnull) NSDictionary *eventProperties; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithEventName:(NSString *)eventName @@ -27,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN location:(CLLocationCoordinate2D)location andItems:(NSArray *)items; +- (instancetype)initWithEventName:(NSString *)eventName + profileAttrName:(NSString *)profileAttrName + eventProperties:(NSDictionary *)eventProperties + andLocation:(CLLocationCoordinate2D)location; + - (CTTriggerValue * _Nullable)propertyValueNamed:(NSString *)name; - (CTTriggerValue * _Nullable)itemValueNamed:(NSString *)name; diff --git a/CleverTapSDK/InApps/Matchers/CTEventAdapter.m b/CleverTapSDK/InApps/Matchers/CTEventAdapter.m index 35df268e..301f551f 100644 --- a/CleverTapSDK/InApps/Matchers/CTEventAdapter.m +++ b/CleverTapSDK/InApps/Matchers/CTEventAdapter.m @@ -14,9 +14,9 @@ @interface CTEventAdapter() @property (nonatomic, strong) NSString *eventName; -@property (nonatomic, strong) NSDictionary *eventProperties; @property (nonatomic, strong) NSArray *items; @property (nonatomic, assign) CLLocationCoordinate2D location; +@property (nonatomic, strong, nullable) NSString *profileAttrName; @end @@ -74,6 +74,18 @@ - (instancetype)initWithEventName:(NSString *)eventName return self; } +- (instancetype)initWithEventName:(NSString *)eventName + profileAttrName:(NSString *)profileAttrName + eventProperties:(NSDictionary *)eventProperties + andLocation:(CLLocationCoordinate2D)location{ + + if (self = [super init]) { + self = [self initWithEventName:eventName eventProperties:eventProperties location:location andItems:@[]]; + self.profileAttrName = profileAttrName; + } + return self; +} + - (CTTriggerValue *)propertyValueNamed:(NSString *)name { id propertyValue = [self getActualPropertyValue:name]; if (propertyValue) { diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h index a500a4a8..6f326107 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSInteger propertyCount; @property (nonatomic, readonly) NSInteger itemsCount; @property (nonatomic, readonly) NSInteger geoRadiusCount; +@property (nonatomic, strong, readonly) NSString *profileAttrName; - (CTTriggerCondition * _Nullable)propertyAtIndex:(NSInteger)index; - (CTTriggerCondition * _Nullable)itemAtIndex:(NSInteger)index; diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m index 83bb7f0f..c6e2eb0a 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m @@ -17,6 +17,7 @@ @interface CTTriggerAdapter() @property (nonatomic, strong) NSArray *items; @property (nonatomic, strong) NSArray *geoRadius; +@property (nonatomic, strong) NSString *profileAttrName; @end @@ -28,6 +29,7 @@ - (instancetype)initWithJSON:(NSDictionary *)triggerJSON { self.properties = triggerJSON[@"eventProperties"]; self.items = triggerJSON[@"itemProperties"]; self.geoRadius = triggerJSON[@"geoRadius"]; + self.profileAttrName = triggerJSON[@"profileAttrName"]; } return self; } diff --git a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m index 1766d49c..b6366b13 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m @@ -30,7 +30,10 @@ - (BOOL)matchEventWhenTriggers:(NSArray *)whenTriggers event:(CTEventAdapter *)e } - (BOOL)match:(CTTriggerAdapter *)trigger event:(CTEventAdapter *)event { - if (![[event eventName] isEqualToString:[trigger eventName]]) { + + BOOL eventNameMatch = [[event eventName] isEqualToString:[trigger eventName]]; + BOOL profileAttrNameMatch = [event profileAttrName] != nil && [[event profileAttrName] isEqualToString:[trigger profileAttrName]]; + if (!eventNameMatch && !profileAttrNameMatch) { return NO; } diff --git a/CleverTapSDKTests/CTLocalDataStoreTests.m b/CleverTapSDKTests/CTLocalDataStoreTests.m new file mode 100644 index 00000000..270d2804 --- /dev/null +++ b/CleverTapSDKTests/CTLocalDataStoreTests.m @@ -0,0 +1,112 @@ +// +// CTLocalDataStoreTests.m +// CleverTapSDKTests +// +// Created by Kushagra Mishra on 04/07/24. +// Copyright © 2024 CleverTap. All rights reserved. +// +#import +#import +#import "CTLocalDataStore.h" +#import "CTProfileBuilder.h" +#import "CTConstants.h" + +@interface CTLocalDataStoreTests : XCTestCase +@property (nonatomic, strong) CTLocalDataStore *dataStore; +@property (nonatomic, strong) id dataStoreMock; +@property (nonatomic, strong) id profileBuilderMock; +@end + +@implementation CTLocalDataStoreTests + +- (void)setUp { + [super setUp]; + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccount" accountToken:@"testToken" accountRegion:@"testRegion"]; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"testDeviceInfo"]; + CTDispatchQueueManager *queueManager = [[CTDispatchQueueManager alloc] initWithConfig:config]; + self.dataStore = [[CTLocalDataStore alloc] initWithConfig:config profileValues:[NSMutableDictionary new] andDeviceInfo:deviceInfo dispatchQueueManager:queueManager]; +} + +- (void)tearDown { + self.dataStore = nil; + [super tearDown]; +} + +- (void)testGetUserAttributeChangePropertiesWithEmptyEvent { + NSDictionary *event = @{}; + NSDictionary *result = [self.dataStore getUserAttributeChangeProperties:event]; + XCTAssertEqual(result.count, 0); +} + +- (void)testGetUserAttributeChangePropertiesWithNoProfile { + NSDictionary *event = @{@"someKey": @"someValue"}; + NSDictionary *result = [self.dataStore getUserAttributeChangeProperties:event]; + XCTAssertEqual(result.count, 0); +} + +- (void)testGetUserAttributeChangePropertiesWithProfileUpdate { + NSDictionary *profile = @{ + @"name": @"John", + @"age": @"30", + @"cc": @"1234", // Should be skipped + @"tz": @"GMT", // Should be skipped + @"Carrier": @"Jio" // Should be skipped + }; + NSDictionary *event = @{CLTAP_PROFILE: profile}; + + // Mock old values for the keys + id mockOldValueForName = @"Jane"; + id mockOldValueForAge = @"25"; + + // Stub the method to return mock old values + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockGetProfileFieldForKeyName = OCMStub([dataStoreMock getProfileFieldForKey:@"name"]).andReturn(mockOldValueForName); + id mockGetProfileFieldForKeyAge = OCMStub([dataStoreMock getProfileFieldForKey:@"age"]).andReturn(mockOldValueForAge); + + // Call the method and get the result + NSDictionary *result = [dataStoreMock getUserAttributeChangeProperties:event]; + + // Verify the result dictionary + XCTAssertEqual(result.count, 2); + XCTAssertEqual(result[@"name"][CLTAP_KEY_OLD_VALUE], mockOldValueForName); + XCTAssertEqual(result[@"name"][CLTAP_KEY_NEW_VALUE], @"John"); + XCTAssertEqual(result[@"age"][CLTAP_KEY_OLD_VALUE], mockOldValueForAge); + XCTAssertEqual(result[@"age"][CLTAP_KEY_NEW_VALUE], @"30"); + + // Ensure skipped keys are not present in the result + XCTAssertNil(result[@"cc"]); + XCTAssertNil(result[@"tz"]); + XCTAssertNil(result[@"Carrier"]); + + // Verify the mock methods were called + OCMVerify(mockGetProfileFieldForKeyName); + OCMVerify(mockGetProfileFieldForKeyAge); +} + +- (void)testGetUserAttributeChangePropertiesWithIncrementCommand { + NSDictionary *profile = @{ + @"points": @{kCLTAP_COMMAND_INCREMENT: @10} + }; + NSDictionary *event = @{CLTAP_PROFILE: profile}; + + // Mock old and new values for the key + id mockOldValue = @20; + id mockNewValue = @30; + + // Stub the method to return mock old values and handle increment command + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockGetProfileFieldForKey = OCMStub([dataStoreMock getProfileFieldForKey:@"points"]).andReturn(mockOldValue); + + // Call the method and get the result + NSDictionary *result = [dataStoreMock getUserAttributeChangeProperties:event]; + + // Verify the result dictionary + XCTAssertEqual(result.count, 1); + XCTAssertEqual(result[@"points"][CLTAP_KEY_OLD_VALUE], mockOldValue); + XCTAssertEqual(result[@"points"][CLTAP_KEY_NEW_VALUE], mockNewValue); + + // Verify the mock methods were called + OCMVerify(mockGetProfileFieldForKey); +} + +@end diff --git a/CleverTapSDKTests/CTUserInfoMigratorTest.m b/CleverTapSDKTests/CTUserInfoMigratorTest.m new file mode 100644 index 00000000..31e0a5cd --- /dev/null +++ b/CleverTapSDKTests/CTUserInfoMigratorTest.m @@ -0,0 +1,107 @@ +// +// CTUserInfoMigratorTests.m +// CleverTapSDKTests +// +// Created by Kushagra Mishra on 21/06/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTUserInfoMigrator.h" +#import "XCTestCase+XCTestCase_Tests.h" + +@interface CTUserInfoMigratorTest : XCTestCase + +@property (nonatomic, strong) NSFileManager *fileManager; +@property (nonatomic, strong) NSString *libraryPath; + +@end + +@implementation CTUserInfoMigratorTest + + +- (void)setUp { + [super setUp]; + self.fileManager = [NSFileManager defaultManager]; + + // Get the path to the Library directory + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + self.libraryPath = [paths objectAtIndex:0]; +} + +- (void)tearDown { + // Clean up any files created during the test + NSError *error = nil; + NSArray *contents = [self.fileManager contentsOfDirectoryAtPath:self.libraryPath error:&error]; + for (NSString *file in contents) { + if ([file containsString:@"clevertap-"]) { + NSString *filePath = [self.libraryPath stringByAppendingPathComponent:file]; + [self.fileManager removeItemAtPath:filePath error:&error]; + } + } + [super tearDown]; +} + +- (void)testMigrateUserInfoFileForAccountID_WhenOldFileExists_ShouldCopyToNewLocation { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Create the old plist file + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager createFileAtPath:oldFilePath contents:[@"old content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the old file has been copied to the new location + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + XCTAssertTrue([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should exist"); + + // Check that the old file has been deleted + XCTAssertFalse([self.fileManager fileExistsAtPath:oldFilePath], @"Old plist file should be deleted"); +} + +- (void)testMigrateUserInfoFileForAccountID_WhenNewFileExists_ShouldNotCopyAndDeleteOldFile { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Create both old and new plist files + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager createFileAtPath:oldFilePath contents:[@"old content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + [self.fileManager createFileAtPath:newFilePath contents:[@"new content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the new file still exists + XCTAssertTrue([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should exist"); + + // Check that the old file has been deleted + XCTAssertFalse([self.fileManager fileExistsAtPath:oldFilePath], @"Old plist file should be deleted"); +} + +- (void)testMigrateUserInfoFileForAccountID_WhenOldFileDoesNotExist_ShouldNotCreateNewFile { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Ensure the old plist file does not exist + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager removeItemAtPath:oldFilePath error:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the new file does not exist + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + XCTAssertFalse([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should not be created"); +} + +@end diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h b/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h index 3c305743..1ce0cf2f 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h @@ -14,6 +14,8 @@ @property (nonatomic, strong) CTInAppDisplayManager *inAppDisplayManager; @property (nonatomic, strong) NSMutableArray *suppressedClientSideInApps; @property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIds; +@property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIdsForProfile; +@property (nonatomic, strong) NSMutableArray *suppressedClientSideInAppsForProfile; @property (nonatomic, strong) NSDictionary *appLaunchedProperties; - (void)sortByPriority:(NSMutableArray *)inApps; - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApps; @@ -24,6 +26,8 @@ - (void)onAppLaunchedWithSuccess:(BOOL)success; - (void)saveEvaluatedServerSideInAppIds; - (void)saveSuppressedClientSideInApps; +- (void)saveEvaluatedServerSideInAppIdsForProfile; +- (void)saveSuppressedClientSideInAppsForProfile; - (NSString *)storageKeyWithSuffix:(NSString *)suffix; @end diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m index eb8c7cd3..f73b7c3e 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m @@ -66,15 +66,20 @@ - (void)setUp { } - (void)tearDown { - // Clean up resources if needed - //self.evaluationManager = nil; + // Remove triggers for (int i = 1; i <= 4; i++) { [self.evaluationManager.triggerManager removeTriggers:[NSString stringWithFormat:@"%d", i]]; } + // Remove saved ids self.evaluationManager.evaluatedServerSideInAppIds = [NSMutableArray new]; [self.evaluationManager saveEvaluatedServerSideInAppIds]; self.evaluationManager.suppressedClientSideInApps = [NSMutableArray new]; [self.evaluationManager saveSuppressedClientSideInApps]; + + self.evaluationManager.evaluatedServerSideInAppIdsForProfile = [NSMutableArray new]; + [self.evaluationManager saveEvaluatedServerSideInAppIdsForProfile]; + self.evaluationManager.suppressedClientSideInAppsForProfile = [NSMutableArray new]; + [self.evaluationManager saveSuppressedClientSideInAppsForProfile]; [super tearDown]; } @@ -299,6 +304,43 @@ - (void)testEvaluateWithInApps { XCTAssertEqual([self.evaluationManager.triggerManager getTriggers:@"4"], 1); } +- (void)testEvaluateUserAttribute { + + self.helper.inAppStore.serverSideInApps = @[ + @{ + @"ti": @1, + @"whenTriggers": @[@{ + @"eventProperties": @[@{ + @"propertyName": @"newValue", + @"propertyValue": @"Gold", + }], + @"profileAttrName": @"Customer Type", + }] + }, + @{ + @"ti": @2, + @"whenTriggers": @[@{ + @"eventProperties": @[@{ + @"propertyName": @"newValue", + @"propertyValue": @"Premium", + }], + @"profileAttrName": @"Customer Type", + }] + }, + ]; + NSDictionary *profile = @{ + @"Customer Type": @{ + @"newValue": @"Gold", + @"oldValue": @"Premium" + } + }; + + + [self.evaluationManager evaluateOnUserAttributeChange:profile]; + XCTAssertEqualObjects((@[@1]), self.evaluationManager.evaluatedServerSideInAppIdsForProfile); + XCTAssertNotEqualObjects((@[@2]), self.evaluationManager.evaluatedServerSideInAppIdsForProfile); +} + - (void)testEvaluateCharged { self.helper.inAppStore.serverSideInApps = @[ @{ @@ -401,7 +443,7 @@ - (void)testEvaluationManagerCaching { CLTAP_INAPP_SUPPRESSED_META_KEY: @[@0] } ]; - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@3]), [self savedEvaluatedServerSideInAppIds]); XCTAssertEqual(1, [[self savedSuppressedClientSideInApps] count]); @@ -646,11 +688,11 @@ - (void)testOnBatchSentRemoveAll { CLTAP_INAPP_SUPPRESSED_META_KEY: @[@4, @5, @6] } ]; - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:NO]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:NO withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@1, @2, @3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@4, @5, @6]), self.evaluationManager.suppressedClientSideInApps); - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[]), self.evaluationManager.suppressedClientSideInApps); } @@ -671,18 +713,18 @@ - (void)testOnBatchSentRemoveElements { } ]; // If batch is not successful, do not remove elements - [self.evaluationManager onBatchSent:batchWithHeader withSuccess:NO]; + [self.evaluationManager onBatchSent:batchWithHeader withSuccess:NO withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@1, @2, @3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@4, @5, @6]), self.evaluationManager.suppressedClientSideInApps); // Remove only the first n elements in the batch - [self.evaluationManager onBatchSent:batchWithHeader withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeader withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@5, @6]), self.evaluationManager.suppressedClientSideInApps); // Remove all elements, ensure no out of range exception // Current values are @[@3] and @[@5, @6] - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[]), self.evaluationManager.suppressedClientSideInApps); }