diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b59361b..e22a6902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,36 @@ # Change Log All notable changes to this project will be documented in this file. +### [Version 5.0.0](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/5.0.0) (May 05, 2023) + +#### Added +- Adds support for Remote Config Variables. Please refer to the [Remote Config Variables doc](/docs/Variables.md) to read more on how to integrate this to your app. + +#### Fixed +- Fixes a bug where the `getLocationWithSuccess` method would cause crashes. +- Adds minor improvements to saving session data in background state. +- Streamlines the argument key of `recordEventWithProps` in `CleverTapJSInterface`. + +#### Deprecated +- The following methods related to Product Config and Feature Flags have been marked as deprecated in this release. These methods will be removed in the future with prior notice + - Feature Flags + - `- (void)ctFeatureFlagsUpdated;` + - `- (BOOL)get:(NSString* _Nonnull)key withDefaultValue:(BOOL)defaultValue` + - Product Config + - `- (void)ctProductConfigFetched` + - `- (void)ctProductConfigActivated` + - `- (void)ctProductConfigInitialized` + - `- (void)fetch` + - `- (void)fetchWithMinimumInterval:(NSTimeInterval)minimumInterval` + - `- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval` + - `- (void)activate` + - `- (void)fetchAndActivate` + - `- (void)setDefaults:(NSDictionary *_Nullable)defaults` + - `- (void)setDefaultsFromPlistFileName:(NSString *_Nullable)fileName` + - `- (CleverTapConfigValue *_Nullable)get:(NSString* _Nonnull)key` + - `- (NSDate *_Nullable)getLastFetchTimeStamp` + - `- (void)reset` + ### [Version 4.2.2](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/4.2.2) (April 03, 2023) #### Fixed diff --git a/CleverTap-iOS-SDK.podspec b/CleverTap-iOS-SDK.podspec index ea1c507f..ecc52732 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.2" +s.version = "5.0.0" s.summary = "The CleverTap iOS SDK for App Analytics and Engagement." s.homepage = "https://github.com/CleverTap/clevertap-ios-sdk" s.license = { :type => "MIT" } @@ -13,9 +13,9 @@ s.ios.dependency 'SDWebImage', '~> 5.11' s.ios.resource_bundle = {'CleverTapSDK' => ['CleverTapSDK/**/*.{png,xib}', 'CleverTapSDK/**/*.xcdatamodeld']} s.ios.deployment_target = '9.0' s.ios.source_files = 'CleverTapSDK/**/*.{h,m}' -s.ios.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h','CleverTapSDK/CleverTap+Inbox.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapInAppNotificationDelegate.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTapJSInterface.h', 'CleverTapSDK/CleverTap+DisplayUnit.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h', 'CleverTapSDK/CleverTapPushNotificationDelegate.h', 'CleverTapSDK/CleverTapURLDelegate.h', 'CleverTapSDK/CleverTap+InAppNotifications.h', 'CleverTapSDK/CleverTap+SCDomain.h', 'CleverTapSDK/CleverTap+PushPermission.h', 'CleverTapSDK/InApps/CTLocalInApp.h' +s.ios.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h','CleverTapSDK/CleverTap+Inbox.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapInAppNotificationDelegate.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTapJSInterface.h', 'CleverTapSDK/CleverTap+DisplayUnit.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h', 'CleverTapSDK/CleverTapPushNotificationDelegate.h', 'CleverTapSDK/CleverTapURLDelegate.h', 'CleverTapSDK/CleverTap+InAppNotifications.h', 'CleverTapSDK/CleverTap+SCDomain.h', 'CleverTapSDK/CleverTap+PushPermission.h', 'CleverTapSDK/InApps/CTLocalInApp.h', 'CleverTapSDK/CleverTap+CTVar.h', 'CleverTapSDK/ProductExperiences/CTVar.h' s.tvos.deployment_target = '9.0' -s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}' +s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}', 'CleverTapSDK/ProductExperiences/*.{h,m}' s.tvos.exclude_files = 'CleverTapSDK/CleverTapJSInterface.{h,m}' -s.tvos.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h' +s.tvos.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h', 'CleverTapSDK/CleverTap+CTVar.h', 'CleverTapSDK/ProductExperiences/CTVar.h' end diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 44120e50..b30c901a 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -157,7 +157,7 @@ 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 */; }; 4808030E292EB4FB00C06E2F /* CleverTap+PushPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */; }; - 48080311292EB50D00C06E2F /* CTLocalInApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030F292EB50D00C06E2F /* CTLocalInApp.h */; }; + 48080311292EB50D00C06E2F /* CTLocalInApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030F292EB50D00C06E2F /* CTLocalInApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; 48080312292EB50D00C06E2F /* CTLocalInApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 48080310292EB50D00C06E2F /* CTLocalInApp.m */; }; 4987C665251B5E79003E6BE8 /* CTImageInAppViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */; }; 4987C666251B5E79003E6BE8 /* CTImageInAppViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */; }; @@ -197,25 +197,68 @@ 4E25E3D02788889E0008C888 /* CTLoginInfoProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E49AE39275CB7670074A774 /* CTLoginInfoProvider.h */; }; 4E25E3D12788889F0008C888 /* CTLoginInfoProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E49AE3A275CB7670074A774 /* CTLoginInfoProvider.m */; }; 4E25E3D22788889F0008C888 /* CTLoginInfoProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E49AE39275CB7670074A774 /* CTLoginInfoProvider.h */; }; + 4E41FD8A294F441D0001FBED /* CTVar.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD89294F441D0001FBED /* CTVar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E41FD8B294F44200001FBED /* CTVar.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD89294F441D0001FBED /* CTVar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E41FD92294F46510001FBED /* CTVar-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD8C294F46500001FBED /* CTVar-Internal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E41FD93294F46510001FBED /* CTVar-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD8C294F46500001FBED /* CTVar-Internal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E41FD94294F46510001FBED /* CTVar.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E41FD8D294F46500001FBED /* CTVar.m */; }; + 4E41FD95294F46510001FBED /* CTVar.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E41FD8D294F46500001FBED /* CTVar.m */; }; + 4E41FD98294F46510001FBED /* CTVarCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD8F294F46510001FBED /* CTVarCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E41FD99294F46510001FBED /* CTVarCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E41FD8F294F46510001FBED /* CTVarCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E41FD9C294F46510001FBED /* CTVarCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E41FD91294F46510001FBED /* CTVarCache.m */; }; + 4E41FD9D294F46510001FBED /* CTVarCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E41FD91294F46510001FBED /* CTVarCache.m */; }; 4E49AE53275D24570074A774 /* CTValidationResultStack.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E49AE51275D24570074A774 /* CTValidationResultStack.h */; }; 4E49AE54275D24570074A774 /* CTValidationResultStack.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E49AE51275D24570074A774 /* CTValidationResultStack.h */; }; 4E49AE55275D24570074A774 /* CTValidationResultStack.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E49AE52275D24570074A774 /* CTValidationResultStack.m */; }; 4E49AE56275D24570074A774 /* CTValidationResultStack.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E49AE52275D24570074A774 /* CTValidationResultStack.m */; }; + 4E6383D7296DE9A8001E83E3 /* CTRequestSender.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E6383D5296DE9A7001E83E3 /* CTRequestSender.h */; }; + 4E6383D8296DE9A8001E83E3 /* CTRequestSender.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E6383D5296DE9A7001E83E3 /* CTRequestSender.h */; }; + 4E6383D9296DE9A8001E83E3 /* CTRequestSender.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E6383D6296DE9A7001E83E3 /* CTRequestSender.m */; }; + 4E6383DA296DE9A8001E83E3 /* CTRequestSender.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E6383D6296DE9A7001E83E3 /* CTRequestSender.m */; }; 4E64855B287440BA00C2F409 /* AmazonRootCA1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4E64855A287440BA00C2F409 /* AmazonRootCA1.cer */; }; 4E64855C287440BA00C2F409 /* AmazonRootCA1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4E64855A287440BA00C2F409 /* AmazonRootCA1.cer */; }; 4E7704B82679DCEF005222D0 /* CleverTapURLDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E7704B72679DCEF005222D0 /* CleverTapURLDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E7929F929799E8F00B81F3C /* CTDomainFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E7929F729799E8F00B81F3C /* CTDomainFactory.h */; }; + 4E7929FA29799E8F00B81F3C /* CTDomainFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E7929F729799E8F00B81F3C /* CTDomainFactory.h */; }; + 4E7929FB29799E8F00B81F3C /* CTDomainFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E7929F829799E8F00B81F3C /* CTDomainFactory.m */; }; + 4E7929FC29799E8F00B81F3C /* CTDomainFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E7929F829799E8F00B81F3C /* CTDomainFactory.m */; }; + 4E838C40299F419900ED0875 /* ContentMerger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E838C3E299F419800ED0875 /* ContentMerger.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E838C41299F419900ED0875 /* ContentMerger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E838C3E299F419800ED0875 /* ContentMerger.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E838C42299F419900ED0875 /* ContentMerger.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E838C3F299F419900ED0875 /* ContentMerger.m */; }; + 4E838C43299F419900ED0875 /* ContentMerger.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E838C3F299F419900ED0875 /* ContentMerger.m */; }; + 4E838C4629A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E838C4429A0C94B00ED0875 /* CleverTap+CTVar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E838C4729A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E838C4429A0C94B00ED0875 /* CleverTap+CTVar.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4E872967277CDF9000A7A618 /* inapp_interstitial.json in Resources */ = {isa = PBXBuildFile; fileRef = 4E1F156427709304009387AE /* inapp_interstitial.json */; }; 4E872968277CDF9000A7A618 /* app_inbox.json in Resources */ = {isa = PBXBuildFile; fileRef = 4E1F156727709848009387AE /* app_inbox.json */; }; 4E872969277CDF9000A7A618 /* inapp_alert.json in Resources */ = {isa = PBXBuildFile; fileRef = 4E1F1561277090D6009387AE /* inapp_alert.json */; }; 4E87296E277CE8EB00A7A618 /* StubHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E87296D277CE8EB00A7A618 /* StubHelper.m */; }; 4E872973277CEE6700A7A618 /* TestConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E872972277CEE6700A7A618 /* TestConstants.m */; }; + 4EA64A26296C115E001D9B22 /* CTRequestFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EA64A24296C115E001D9B22 /* CTRequestFactory.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EA64A27296C115E001D9B22 /* CTRequestFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EA64A24296C115E001D9B22 /* CTRequestFactory.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EA64A28296C115E001D9B22 /* CTRequestFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A25296C115E001D9B22 /* CTRequestFactory.m */; }; + 4EA64A29296C115E001D9B22 /* CTRequestFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A25296C115E001D9B22 /* CTRequestFactory.m */; }; + 4EA64A2C296C1190001D9B22 /* CTRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EA64A2A296C1190001D9B22 /* CTRequest.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EA64A2D296C1190001D9B22 /* CTRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EA64A2A296C1190001D9B22 /* CTRequest.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EA64A2E296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; + 4EA64A2F296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; 4EC2D085278AAD8000F4DE54 /* IdentityManagementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC2D084278AAD8000F4DE54 /* IdentityManagementTests.m */; }; + 4EED219B29AF6368006CEA19 /* CTVarCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EED219A29AF6368006CEA19 /* CTVarCacheTest.m */; }; + 4EF1CF9E29B076A300E3CB6A /* CTVarCache+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EF1CF9D29B076A300E3CB6A /* CTVarCache+Tests.m */; }; 5709005327FD8E1F0011B89F /* CleverTap+SCDomain.h in Headers */ = {isa = PBXBuildFile; fileRef = 5709005227FD8E1E0011B89F /* CleverTap+SCDomain.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57D2E1C82684B1630068E45A /* CleverTap.h in Headers */ = {isa = PBXBuildFile; fileRef = D0C7BBC8207D8837001345EF /* CleverTap.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57EDC7A12683845B001DD157 /* CleverTap+InAppNotifications.h in Headers */ = {isa = PBXBuildFile; fileRef = 57EDC7A02683845B001DD157 /* CleverTap+InAppNotifications.h */; settings = {ATTRIBUTES = (Public, ); }; }; 583D9573DCDA5D219016B990 /* libPods-shared-CleverTapSDKTestsApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2108AFE9090417CC69A0E3EB /* libPods-shared-CleverTapSDKTestsApp.a */; }; 63359A5791C468263B934E22 /* libPods-shared-CleverTapSDKTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 917B24847F7C2A8B0825AEC1 /* libPods-shared-CleverTapSDKTests.a */; }; + 6A2E0B9129CCCC8600FCEA5F /* ContentMergerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E0B9029CCCC8500FCEA5F /* ContentMergerTest.m */; }; + 6A2E0B9329D0A5CF00FCEA5F /* CTVariablesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E0B9229D0A5CE00FCEA5F /* CTVariablesTest.m */; }; + 6A2E0B9529D49D0200FCEA5F /* CTVariables+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E0B9429D49D0200FCEA5F /* CTVariables+Tests.m */; }; + 6A2E0B9829D49D5100FCEA5F /* CTVarCacheMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E0B9729D49D5100FCEA5F /* CTVarCacheMock.m */; }; 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E4C17291E8A4A00385536 /* CleverTapInstanceConfigTests.m */; }; + 6A775C3329BE78C7007790E0 /* CTVariables.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A775C3129BE78C7007790E0 /* CTVariables.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6A775C3429BE78C7007790E0 /* CTVariables.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A775C3129BE78C7007790E0 /* CTVariables.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6A775C3529BE78C7007790E0 /* CTVariables.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A775C3229BE78C7007790E0 /* CTVariables.m */; }; + 6A775C3629BE78C7007790E0 /* CTVariables.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A775C3229BE78C7007790E0 /* CTVariables.m */; }; + 6A7BB8DC29E47CFF00651584 /* CTVarTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A7BB8DB29E47CFF00651584 /* CTVarTest.m */; }; D0047B0A2098D45B0019C6FD /* CTLocalDataStore.h in Headers */ = {isa = PBXBuildFile; fileRef = D0047B082098D45B0019C6FD /* CTLocalDataStore.h */; settings = {ATTRIBUTES = (Private, ); }; }; D0047B0B2098D45B0019C6FD /* CTLocalDataStore.m in Sources */ = {isa = PBXBuildFile; fileRef = D0047B092098D45B0019C6FD /* CTLocalDataStore.m */; }; D0047B0E2098E2F00019C6FD /* CTProfileBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = D0047B0C2098E2F00019C6FD /* CTProfileBuilder.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -567,6 +610,11 @@ 4E1F1561277090D6009387AE /* inapp_alert.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = inapp_alert.json; sourceTree = ""; }; 4E1F156427709304009387AE /* inapp_interstitial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = inapp_interstitial.json; sourceTree = ""; }; 4E1F156727709848009387AE /* app_inbox.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = app_inbox.json; sourceTree = ""; }; + 4E41FD89294F441D0001FBED /* CTVar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTVar.h; sourceTree = ""; }; + 4E41FD8C294F46500001FBED /* CTVar-Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CTVar-Internal.h"; sourceTree = ""; }; + 4E41FD8D294F46500001FBED /* CTVar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTVar.m; sourceTree = ""; }; + 4E41FD8F294F46510001FBED /* CTVarCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTVarCache.h; sourceTree = ""; }; + 4E41FD91294F46510001FBED /* CTVarCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTVarCache.m; sourceTree = ""; }; 4E49AE39275CB7670074A774 /* CTLoginInfoProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTLoginInfoProvider.h; sourceTree = ""; }; 4E49AE3A275CB7670074A774 /* CTLoginInfoProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTLoginInfoProvider.m; sourceTree = ""; }; 4E49AE42275D00E80074A774 /* CTIdentityRepo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTIdentityRepo.h; sourceTree = ""; }; @@ -578,17 +626,40 @@ 4E49AE52275D24570074A774 /* CTValidationResultStack.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTValidationResultStack.m; sourceTree = ""; }; 4E49AE57275D31910074A774 /* CTIdentityRepoFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTIdentityRepoFactory.h; sourceTree = ""; }; 4E49AE58275D31910074A774 /* CTIdentityRepoFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTIdentityRepoFactory.m; sourceTree = ""; }; + 4E6383D5296DE9A7001E83E3 /* CTRequestSender.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTRequestSender.h; sourceTree = ""; }; + 4E6383D6296DE9A7001E83E3 /* CTRequestSender.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTRequestSender.m; sourceTree = ""; }; 4E64855A287440BA00C2F409 /* AmazonRootCA1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AmazonRootCA1.cer; sourceTree = ""; }; 4E7704B72679DCEF005222D0 /* CleverTapURLDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CleverTapURLDelegate.h; sourceTree = ""; }; + 4E7929F729799E8F00B81F3C /* CTDomainFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTDomainFactory.h; sourceTree = ""; }; + 4E7929F829799E8F00B81F3C /* CTDomainFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTDomainFactory.m; sourceTree = ""; }; + 4E838C3E299F419800ED0875 /* ContentMerger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContentMerger.h; sourceTree = ""; }; + 4E838C3F299F419900ED0875 /* ContentMerger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContentMerger.m; sourceTree = ""; }; + 4E838C4429A0C94B00ED0875 /* CleverTap+CTVar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CleverTap+CTVar.h"; sourceTree = ""; }; 4E87296C277CE8EB00A7A618 /* StubHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StubHelper.h; sourceTree = ""; }; 4E87296D277CE8EB00A7A618 /* StubHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StubHelper.m; sourceTree = ""; }; 4E872971277CEE6700A7A618 /* TestConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestConstants.h; sourceTree = ""; }; 4E872972277CEE6700A7A618 /* TestConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestConstants.m; sourceTree = ""; }; + 4EA64A24296C115E001D9B22 /* CTRequestFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTRequestFactory.h; sourceTree = ""; }; + 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 = ""; }; 4EC2D084278AAD8000F4DE54 /* IdentityManagementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IdentityManagementTests.m; sourceTree = ""; }; 4EDCDE4D278AC4DF0065E699 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 4EED219A29AF6368006CEA19 /* CTVarCacheTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTVarCacheTest.m; sourceTree = ""; }; + 4EF1CF9C29B076A300E3CB6A /* CTVarCache+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTVarCache+Tests.h"; sourceTree = ""; }; + 4EF1CF9D29B076A300E3CB6A /* CTVarCache+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CTVarCache+Tests.m"; sourceTree = ""; }; 5709005227FD8E1E0011B89F /* CleverTap+SCDomain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CleverTap+SCDomain.h"; sourceTree = ""; }; 57EDC7A02683845B001DD157 /* CleverTap+InAppNotifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CleverTap+InAppNotifications.h"; sourceTree = ""; }; + 6A2E0B9029CCCC8500FCEA5F /* ContentMergerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContentMergerTest.m; sourceTree = ""; }; + 6A2E0B9229D0A5CE00FCEA5F /* CTVariablesTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTVariablesTest.m; sourceTree = ""; }; + 6A2E0B9429D49D0200FCEA5F /* CTVariables+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CTVariables+Tests.m"; sourceTree = ""; }; + 6A2E0B9629D49D5100FCEA5F /* CTVarCacheMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTVarCacheMock.h; sourceTree = ""; }; + 6A2E0B9729D49D5100FCEA5F /* CTVarCacheMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTVarCacheMock.m; sourceTree = ""; }; + 6A2E0B9929D49E4700FCEA5F /* CTVariables+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTVariables+Tests.h"; sourceTree = ""; }; 6A2E4C17291E8A4A00385536 /* CleverTapInstanceConfigTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CleverTapInstanceConfigTests.m; sourceTree = ""; }; + 6A775C3129BE78C7007790E0 /* CTVariables.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTVariables.h; sourceTree = ""; }; + 6A775C3229BE78C7007790E0 /* CTVariables.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTVariables.m; sourceTree = ""; }; + 6A7BB8DB29E47CFF00651584 /* CTVarTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTVarTest.m; sourceTree = ""; }; 840B38DDFBBEAE05FC6C5458 /* Pods-shared-CleverTapSDKTestsApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTestsApp.release.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTestsApp/Pods-shared-CleverTapSDKTestsApp.release.xcconfig"; sourceTree = ""; }; 917B24847F7C2A8B0825AEC1 /* libPods-shared-CleverTapSDKTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-shared-CleverTapSDKTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A1765572FACEA4B081A89F82 /* Pods-shared-CleverTapSDKTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTests.debug.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTests/Pods-shared-CleverTapSDKTests.debug.xcconfig"; sourceTree = ""; }; @@ -979,9 +1050,43 @@ path = "Stub Responses"; sourceTree = ""; }; + 4E41FD88294F43E70001FBED /* ProductExperiences */ = { + isa = PBXGroup; + children = ( + 4E41FD8C294F46500001FBED /* CTVar-Internal.h */, + 4E41FD89294F441D0001FBED /* CTVar.h */, + 4E41FD8D294F46500001FBED /* CTVar.m */, + 4E41FD8F294F46510001FBED /* CTVarCache.h */, + 4E41FD91294F46510001FBED /* CTVarCache.m */, + 4E838C3E299F419800ED0875 /* ContentMerger.h */, + 4E838C3F299F419900ED0875 /* ContentMerger.m */, + 6A775C3129BE78C7007790E0 /* CTVariables.h */, + 6A775C3229BE78C7007790E0 /* CTVariables.m */, + ); + path = ProductExperiences; + sourceTree = ""; + }; + 6A7BB8DE29E60BE900651584 /* ProductExperiences */ = { + isa = PBXGroup; + children = ( + 4EF1CF9C29B076A300E3CB6A /* CTVarCache+Tests.h */, + 4EF1CF9D29B076A300E3CB6A /* CTVarCache+Tests.m */, + 4EED219A29AF6368006CEA19 /* CTVarCacheTest.m */, + 6A2E0B9029CCCC8500FCEA5F /* ContentMergerTest.m */, + 6A2E0B9229D0A5CE00FCEA5F /* CTVariablesTest.m */, + 6A2E0B9429D49D0200FCEA5F /* CTVariables+Tests.m */, + 6A2E0B9629D49D5100FCEA5F /* CTVarCacheMock.h */, + 6A2E0B9729D49D5100FCEA5F /* CTVarCacheMock.m */, + 6A2E0B9929D49E4700FCEA5F /* CTVariables+Tests.h */, + 6A7BB8DB29E47CFF00651584 /* CTVarTest.m */, + ); + path = ProductExperiences; + sourceTree = ""; + }; D02AC2D9276044F70031C1BE /* CleverTapSDKTests */ = { isa = PBXGroup; children = ( + 6A7BB8DE29E60BE900651584 /* ProductExperiences */, 4EDCDE4D278AC4DF0065E699 /* Info.plist */, 4E1F1560277090D6009387AE /* Stub Responses */, D02AC2EC2767F4D10031C1BE /* BaseTestCase.h */, @@ -1088,7 +1193,9 @@ isa = PBXGroup; children = ( 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */, + 4E838C4429A0C94B00ED0875 /* CleverTap+CTVar.h */, 4E64855A287440BA00C2F409 /* AmazonRootCA1.cer */, + 4E41FD88294F43E70001FBED /* ProductExperiences */, D0BD7598241760A30006EE55 /* ProductConfig */, D0D4C9E62414CBA30029477E /* FeatureFlags */, 0701E9572372AD550034AAC2 /* DisplayUnit */, @@ -1177,6 +1284,14 @@ 071EB4A1217F6427008F0FAB /* CTInAppDisplayViewControllerPrivate.h */, 071EB4AA217F6427008F0FAB /* CTNotificationButton.h */, 071EB481217F6427008F0FAB /* CTNotificationButton.m */, + 4EA64A24296C115E001D9B22 /* CTRequestFactory.h */, + 4EA64A25296C115E001D9B22 /* CTRequestFactory.m */, + 4EA64A2A296C1190001D9B22 /* CTRequest.h */, + 4EA64A2B296C1190001D9B22 /* CTRequest.m */, + 4E6383D5296DE9A7001E83E3 /* CTRequestSender.h */, + 4E6383D6296DE9A7001E83E3 /* CTRequestSender.m */, + 4E7929F729799E8F00B81F3C /* CTDomainFactory.h */, + 4E7929F829799E8F00B81F3C /* CTDomainFactory.m */, ); path = CleverTapSDK; sourceTree = ""; @@ -1226,16 +1341,19 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 4E41FD8B294F44200001FBED /* CTVar.h in Headers */, D014B90620E2FB62001E0780 /* CTKnownProfileFields.h in Headers */, D014B8E820E2FA6D001E0780 /* CleverTapBuildInfo.h in Headers */, D014B90020E2FB46001E0780 /* CTValidationResult.h in Headers */, D014B8E320E2F9FA001E0780 /* CleverTapInstanceConfigPrivate.h in Headers */, D014B91E20E2FBDA001E0780 /* CTPinnedNSURLSessionDelegate.h in Headers */, + 4E41FD93294F46510001FBED /* CTVar-Internal.h in Headers */, D014B8E920E2FA88001E0780 /* CleverTapEventDetail.h in Headers */, 071EB521217F6764008F0FAB /* CTNotificationButton.h in Headers */, D014B8FC20E2FB12001E0780 /* CTPreferences.h in Headers */, 071EB518217F6510008F0FAB /* CTInAppFCManager.h in Headers */, D014B8EE20E2FAA8001E0780 /* CleverTapUTMDetail.h in Headers */, + 4E838C4729A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */, D014B90820E2FB6B001E0780 /* CTLocalDataStore.h in Headers */, D0BD75A12417690E0006EE55 /* CleverTapProductConfigPrivate.h in Headers */, D014B90220E2FB4F001E0780 /* CTEventBuilder.h in Headers */, @@ -1246,10 +1364,12 @@ D014B8F020E2FAB1001E0780 /* CTPlistInfo.h in Headers */, D014B8EC20E2FA9D001E0780 /* CleverTapTrackedViewController.h in Headers */, D0BD75AC241769710006EE55 /* CleverTap+FeatureFlags.h in Headers */, + 4E7929FA29799E8F00B81F3C /* CTDomainFactory.h in Headers */, 4E25E3CC278887A80008C888 /* CTFlexibleIdentityRepo.h in Headers */, D014B8EB20E2FA94001E0780 /* CleverTapSyncDelegate.h in Headers */, 4E49AE54275D24570074A774 /* CTValidationResultStack.h in Headers */, 07BF465D217F7C88002E166D /* CTInAppDisplayViewControllerPrivate.h in Headers */, + 4E838C41299F419900ED0875 /* ContentMerger.h in Headers */, D0BD75A82417694F0006EE55 /* CTFeatureFlagsController.h in Headers */, 4E25E3CD278887A80008C888 /* CTLegacyIdentityRepo.h in Headers */, D014B8F820E2FADD001E0780 /* CTUtils.h in Headers */, @@ -1261,12 +1381,17 @@ 49E2B18824237DCB00AD704B /* CleverTapFeatureFlagsPrivate.h in Headers */, 071EB525217F6C8F008F0FAB /* CTUIUtils.h in Headers */, D014B8FE20E2FB3B001E0780 /* CTLogger.h in Headers */, + 4EA64A2D296C1190001D9B22 /* CTRequest.h in Headers */, D014B8E120E2F9E9001E0780 /* CleverTap+SSLPinning.h in Headers */, D014B91C20E2FBD1001E0780 /* CTCertificatePinning.h in Headers */, D014B90420E2FB59001E0780 /* CTValidator.h in Headers */, + 4E6383D8296DE9A8001E83E3 /* CTRequestSender.h in Headers */, D0A9E9CD20EEA8630004BC6F /* CTLocationManager.h in Headers */, + 4EA64A27296C115E001D9B22 /* CTRequestFactory.h in Headers */, D014B8F420E2FACA001E0780 /* CTSwizzle.h in Headers */, + 6A775C3429BE78C7007790E0 /* CTVariables.h in Headers */, D014B8E620E2FA64001E0780 /* CleverTapInstanceConfig.h in Headers */, + 4E41FD99294F46510001FBED /* CTVarCache.h in Headers */, D014B8F220E2FABC001E0780 /* CTDeviceInfo.h in Headers */, D014B90A20E2FB74001E0780 /* CTProfileBuilder.h in Headers */, 07BF4662217F8359002E166D /* CTInAppUtils.h in Headers */, @@ -1278,6 +1403,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 4EA64A2C296C1190001D9B22 /* CTRequest.h in Headers */, 57D2E1C82684B1630068E45A /* CleverTap.h in Headers */, D0CACF8B20B8923700A02327 /* CleverTap+SSLPinning.h in Headers */, 4E7704B82679DCEF005222D0 /* CleverTapURLDelegate.h in Headers */, @@ -1290,8 +1416,10 @@ D014B8E220E2F9F9001E0780 /* CleverTapInstanceConfigPrivate.h in Headers */, 071EB4CF217F6427008F0FAB /* CTBaseHeaderFooterViewControllerPrivate.h in Headers */, D0BD759D241760C60006EE55 /* CTProductConfigController.h in Headers */, + 4E41FD8A294F441D0001FBED /* CTVar.h in Headers */, 4E49AE53275D24570074A774 /* CTValidationResultStack.h in Headers */, 07B94544219EA34300D4C542 /* CTInboxController.h in Headers */, + 4E838C40299F419900ED0875 /* ContentMerger.h in Headers */, 5709005327FD8E1F0011B89F /* CleverTap+SCDomain.h in Headers */, D0213D4F207D905800FE5740 /* CleverTapUTMDetail.h in Headers */, 071EB4D3217F6427008F0FAB /* CTAVPlayerViewController.h in Headers */, @@ -1300,13 +1428,17 @@ 07B94546219EA34300D4C542 /* CTMessageMO+CoreDataProperties.h in Headers */, D01651B22097B42C00660178 /* CTValidator.h in Headers */, D0A84ADC209136D500191B1F /* CTLogger.h in Headers */, + 4E41FD98294F46510001FBED /* CTVarCache.h in Headers */, 07B94541219EA34300D4C542 /* CleverTapInboxViewControllerPrivate.h in Headers */, 071EB4EE217F6427008F0FAB /* CTAlertViewController.h in Headers */, + 4E838C4629A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */, D0CACF9620B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h in Headers */, + 4E41FD92294F46510001FBED /* CTVar-Internal.h in Headers */, 071EB4FE217F6427008F0FAB /* CTDismissButton.h in Headers */, 071EB511217F6427008F0FAB /* CTUIUtils.h in Headers */, D01651AE2097B38400660178 /* CTEventBuilder.h in Headers */, 072F9E3D21B1368000BC6313 /* CTInboxIconMessageCell.h in Headers */, + 4EA64A26296C115E001D9B22 /* CTRequestFactory.h in Headers */, D01651B62097B81400660178 /* CTKnownProfileFields.h in Headers */, 071EB4D7217F6427008F0FAB /* CTCoverViewController.h in Headers */, D0047B0E2098E2F00019C6FD /* CTProfileBuilder.h in Headers */, @@ -1318,6 +1450,8 @@ 07D8C08B21DDEC54006F5A1B /* CTCarouselImageView.h in Headers */, 071EB515217F6427008F0FAB /* CTInterstitialViewController.h in Headers */, 0701E9622372C1950034AAC2 /* CTDisplayUnitController.h in Headers */, + 4E7929F929799E8F00B81F3C /* CTDomainFactory.h in Headers */, + 6A775C3329BE78C7007790E0 /* CTVariables.h in Headers */, 0797132F21A2F09A0011C9A3 /* CTSwipeView.h in Headers */, D0BD75A02417690E0006EE55 /* CleverTapProductConfigPrivate.h in Headers */, D0D4C9EB2414D1C50029477E /* CTFeatureFlagsController.h in Headers */, @@ -1344,6 +1478,7 @@ 071EB4FB217F6427008F0FAB /* CTInAppDisplayViewController.h in Headers */, D0A6626D20801E7F00B403F3 /* CTDeviceInfo.h in Headers */, D0A6626A207EE6F500B403F3 /* CleverTapBuildInfo.h in Headers */, + 4E6383D7296DE9A8001E83E3 /* CTRequestSender.h in Headers */, D0D4C9F52414EE770029477E /* CleverTapFeatureFlagsPrivate.h in Headers */, D0213D50207D905800FE5740 /* CleverTapEventDetail.h in Headers */, 07B94550219EA39000D4C542 /* CleverTap+Inbox.h in Headers */, @@ -1696,7 +1831,10 @@ files = ( 49C189A3243B0CBB0003E4D4 /* CTFeatureFlagsController.m in Sources */, 071EB519217F6513008F0FAB /* CTInAppFCManager.m in Sources */, + 4EA64A2F296C1190001D9B22 /* CTRequest.m in Sources */, 49C189A6243B13110003E4D4 /* CleverTapFeatureFlags.m in Sources */, + 4E41FD95294F46510001FBED /* CTVar.m in Sources */, + 4E6383DA296DE9A8001E83E3 /* CTRequestSender.m in Sources */, D014B8ED20E2FAA2001E0780 /* CleverTapTrackedViewController.m in Sources */, D014B8F120E2FAB9001E0780 /* CTPlistInfo.m in Sources */, 4E25E3D12788889F0008C888 /* CTLoginInfoProvider.m in Sources */, @@ -1705,7 +1843,10 @@ 07BF4661217F80C1002E166D /* CTUIUtils.m in Sources */, 07BF465C217F7C4B002E166D /* CTInAppDisplayViewController.m in Sources */, 071EB51B217F6568008F0FAB /* CTInAppNotification.m in Sources */, + 6A775C3629BE78C7007790E0 /* CTVariables.m in Sources */, D0A9E9CE20EEA8770004BC6F /* CTLocationManager.m in Sources */, + 4E838C43299F419900ED0875 /* ContentMerger.m in Sources */, + 4E7929FC29799E8F00B81F3C /* CTDomainFactory.m in Sources */, D014B90B20E2FB7A001E0780 /* CTProfileBuilder.m in Sources */, D014B90120E2FB4C001E0780 /* CTValidationResult.m in Sources */, 4E25E3CB278887A80008C888 /* CTLegacyIdentityRepo.m in Sources */, @@ -1720,12 +1861,14 @@ D014B8E720E2FA6A001E0780 /* CleverTapInstanceConfig.m in Sources */, D014B8F520E2FAD0001E0780 /* CTSwizzle.m in Sources */, D014B8EF20E2FAAD001E0780 /* CleverTapUTMDetail.m in Sources */, + 4E41FD9D294F46510001FBED /* CTVarCache.m in Sources */, D014B91D20E2FBD6001E0780 /* CTCertificatePinning.m in Sources */, D014B90920E2FB71001E0780 /* CTLocalDataStore.m in Sources */, D0BD75A7241769440006EE55 /* CleverTapProductConfig.m in Sources */, D014B8EA20E2FA8D001E0780 /* CleverTapEventDetail.m in Sources */, D014B90320E2FB55001E0780 /* CTEventBuilder.m in Sources */, D014B8FB20E2FB0E001E0780 /* CTUriHelper.m in Sources */, + 4EA64A29296C115E001D9B22 /* CTRequestFactory.m in Sources */, D014B8FF20E2FB40001E0780 /* CTLogger.m in Sources */, D014B8FD20E2FB18001E0780 /* CTPreferences.m in Sources */, 4E49AE56275D24570074A774 /* CTValidationResultStack.m in Sources */, @@ -1741,10 +1884,17 @@ files = ( 4E1F154F27691CA0009387AE /* CleverTapInstanceTests.m in Sources */, 4E1F155B276B662C009387AE /* EventDetail.m in Sources */, + 4EED219B29AF6368006CEA19 /* CTVarCacheTest.m in Sources */, + 6A7BB8DC29E47CFF00651584 /* CTVarTest.m in Sources */, + 6A2E0B9129CCCC8600FCEA5F /* ContentMergerTest.m in Sources */, + 6A2E0B9529D49D0200FCEA5F /* CTVariables+Tests.m in Sources */, + 6A2E0B9829D49D5100FCEA5F /* CTVarCacheMock.m in Sources */, + 6A2E0B9329D0A5CF00FCEA5F /* CTVariablesTest.m in Sources */, D02AC2DB276044F70031C1BE /* CleverTapSDKTests.m in Sources */, D02AC2EB2767F4590031C1BE /* BaseTestCase.m in Sources */, 4E1F15532769B83D009387AE /* EventTests.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, + 4EF1CF9E29B076A300E3CB6A /* CTVarCache+Tests.m in Sources */, 4EC2D085278AAD8000F4DE54 /* IdentityManagementTests.m in Sources */, 4E1F155227692A11009387AE /* CleverTap+Tests.m in Sources */, ); @@ -1778,6 +1928,7 @@ files = ( 071EB509217F6427008F0FAB /* CTAlertViewController.m in Sources */, 07B9454B219EA34300D4C542 /* CleverTapInboxMessage.m in Sources */, + 4E41FD9C294F46510001FBED /* CTVarCache.m in Sources */, 07053B7321E653E70085B44A /* UIView+CTToast.m in Sources */, D0BD75A6241769440006EE55 /* CleverTapProductConfig.m in Sources */, D0213D52207D905800FE5740 /* CleverTapEventDetail.m in Sources */, @@ -1791,15 +1942,18 @@ D079742A21FE2F2300773602 /* CTVideoThumbnailGenerator.m in Sources */, 071EB4C9217F6427008F0FAB /* CTHeaderViewController.m in Sources */, 4E25E3C7278887A70008C888 /* CTIdentityRepoFactory.m in Sources */, + 4E41FD94294F46510001FBED /* CTVar.m in Sources */, 071EB505217F6427008F0FAB /* CTCoverViewController.m in Sources */, 071EB4CD217F6427008F0FAB /* CTInAppUtils.m in Sources */, D0CACF9120B8A44C00A02327 /* CTCertificatePinning.m in Sources */, + 4EA64A28296C115E001D9B22 /* CTRequestFactory.m in Sources */, D033FB84208FE51200B4390F /* CTUtils.m in Sources */, 0701E975237A9A760034AAC2 /* CleverTapDisplayUnitContent.m in Sources */, 4987C666251B5E79003E6BE8 /* CTImageInAppViewController.m in Sources */, D0405B4722050C5200D64EC3 /* CTInboxUtils.m in Sources */, D01651B32097B42C00660178 /* CTValidator.m in Sources */, 49C189A2243B08A40003E4D4 /* CTFeatureFlagsController.m in Sources */, + 4E838C42299F419900ED0875 /* ContentMerger.m in Sources */, 071EB513217F6427008F0FAB /* CTInAppNotification.m in Sources */, D0CACF9720B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.m in Sources */, D0047B0F2098E2F00019C6FD /* CTProfileBuilder.m in Sources */, @@ -1807,6 +1961,7 @@ 0797133A21A309720011C9A3 /* CTCarouselImageView.m in Sources */, 072F9E4321B14ECC00BC6313 /* CTInboxMessageActionView.m in Sources */, 0796FB6A21AE5B6300FC380D /* CTCarouselImageMessageCell.m in Sources */, + 4E6383D9296DE9A8001E83E3 /* CTRequestSender.m in Sources */, 071EB4D1217F6427008F0FAB /* CTNotificationButton.m in Sources */, 0701E9632372C1950034AAC2 /* CTDisplayUnitController.m in Sources */, D0213D4E207D905800FE5740 /* CleverTapTrackedViewController.m in Sources */, @@ -1814,6 +1969,7 @@ D0047B0B2098D45B0019C6FD /* CTLocalDataStore.m in Sources */, 071EB4CC217F6427008F0FAB /* CTHalfInterstitialViewController.m in Sources */, 07B94549219EA34300D4C542 /* CTMessageMO+CoreDataProperties.m in Sources */, + 4EA64A2E296C1190001D9B22 /* CTRequest.m in Sources */, 071EB4CE217F6427008F0FAB /* CTInAppDisplayViewController.m in Sources */, 07B9454D219EA34300D4C542 /* Inbox.xcdatamodeld in Sources */, D0213D51207D905800FE5740 /* CleverTapUTMDetail.m in Sources */, @@ -1828,6 +1984,8 @@ 071EB4D4217F6427008F0FAB /* CTBaseHeaderFooterViewController.m in Sources */, D06F052921E802D400D1B6BD /* CTInboxBaseMessageCell.m in Sources */, 07B9453F219EA34300D4C542 /* CleverTapInboxStyleConfig.m in Sources */, + 6A775C3529BE78C7007790E0 /* CTVariables.m in Sources */, + 4E7929FB29799E8F00B81F3C /* CTDomainFactory.m in Sources */, 0796FB6421AE5B2900FC380D /* CTCarouselMessageCell.m in Sources */, 071EB4F6217F6427008F0FAB /* CTUIUtils.m in Sources */, 071EB4FA217F6427008F0FAB /* CTAVPlayerViewController.m in Sources */, @@ -1914,6 +2072,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO; INFOPLIST_FILE = "CleverTapSDK/tvOS-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -1947,6 +2106,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO; INFOPLIST_FILE = "CleverTapSDK/tvOS-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -2303,6 +2463,7 @@ "DEBUG=1", "$(inherited)", ); + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO; INFOPLIST_FILE = CleverTapSDK/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -2340,6 +2501,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/Vendors", ); + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO; INFOPLIST_FILE = CleverTapSDK/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; diff --git a/CleverTapSDK/CTConstants.h b/CleverTapSDK/CTConstants.h index 91f38adc..7ae9d40e 100644 --- a/CleverTapSDK/CTConstants.h +++ b/CleverTapSDK/CTConstants.h @@ -2,6 +2,22 @@ extern NSString *const kCTApiDomain; extern NSString *const kCTNotifViewedApiDomain; +extern NSString *const kHANDSHAKE_URL; +extern NSString *CT_KIND_INT; +extern NSString *CT_KIND_FLOAT; +extern NSString *CT_KIND_STRING; +extern NSString *CT_KIND_BOOLEAN; +extern NSString *CT_KIND_DICTIONARY; +extern NSString *CLEVERTAP_DEFAULTS_VARIABLES_KEY; +extern NSString *CLEVERTAP_DEFAULTS_VARS_JSON_KEY; + +extern NSString *CT_PE_DEFINE_VARS_ENDPOINT; +extern NSString *CT_PE_VARS_PAYLOAD_TYPE; +extern NSString *CT_PE_VARS_PAYLOAD_KEY; +extern NSString *CT_PE_VAR_TYPE; +extern NSString *CT_PE_NUMBER_TYPE; +extern NSString *CT_PE_BOOL_TYPE; +extern NSString *CT_PE_DEFAULT_VALUE; #define CleverTapLogInfo(level, fmt, ...) if(level >= 0) { NSLog((@"%@" fmt), @"[CleverTap]: ", ##__VA_ARGS__); } #define CleverTapLogDebug(level, fmt, ...) if(level > 0) { NSLog((@"%@" fmt), @"[CleverTap]: ", ##__VA_ARGS__); } @@ -10,6 +26,13 @@ extern NSString *const kCTNotifViewedApiDomain; #define CleverTapLogStaticDebug(fmt, ...) if([CTLogger getDebugLevel] > 0) { NSLog((@"%@" fmt), @"[CleverTap]: ", ##__VA_ARGS__); } #define CleverTapLogStaticInternal(fmt, ...) if([CTLogger getDebugLevel] > 1) { NSLog((@"%@" fmt), @"[CleverTap]: ", ##__VA_ARGS__); } + + +#define CT_TRY @try { +#define CT_END_TRY }\ +@catch (NSException *e) {\ +[CTLogger logInternalError:e]; } + #define CLTAP_REQUEST_TIME_OUT_INTERVAL 10 #define CLTAP_ACCOUNT_ID_LABEL @"CleverTapAccountID" #define CLTAP_TOKEN_LABEL @"CleverTapToken" @@ -63,6 +86,7 @@ extern NSString *const kCTNotifViewedApiDomain; #define CLTAP_PRODUCT_CONFIG_JSON_RESPONSE_KEY @"pc_notifs" #define CLTAP_PREFS_INAPP_KEY @"inapp_notifs" #define CLTAP_GEOFENCES_JSON_RESPONSE_KEY @"geofences" +#define CLTAP_PE_VARS_RESPONSE_KEY @"vars" #define CLTAP_DISCARDED_EVENT_JSON_KEY @"d_e" #define CLTAP_INAPP_CLOSE_IV_WIDTH 40 #define CLTAP_NOTIFICATION_ID_TAG @"wzrk_id" @@ -124,4 +148,6 @@ extern NSString *const kCTNotifViewedApiDomain; #define CLTAP_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email"] // LEGACY KEYS #define CLTAP_ALL_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email", @"Phone"] +#define CLTAP_DEFINE_VARS_URL @"/defineVars" + diff --git a/CleverTapSDK/CTConstants.m b/CleverTapSDK/CTConstants.m index e01145d7..c8b5d773 100644 --- a/CleverTapSDK/CTConstants.m +++ b/CleverTapSDK/CTConstants.m @@ -2,3 +2,21 @@ NSString *const kCTApiDomain = @"clevertap-prod.com"; NSString *const kCTNotifViewedApiDomain = @"spiky.clevertap-prod.com"; +NSString *const kHANDSHAKE_URL = @"https://clevertap-prod.com/hello"; + +NSString *CT_KIND_INT = @"integer"; +NSString *CT_KIND_FLOAT = @"float"; +NSString *CT_KIND_STRING = @"string"; +NSString *CT_KIND_BOOLEAN = @"bool"; +NSString *CT_KIND_DICTIONARY = @"group"; +NSString *CLEVERTAP_DEFAULTS_VARIABLES_KEY = @"__clevertap_variables"; +NSString *CLEVERTAP_DEFAULTS_VARS_JSON_KEY = @"__clevertap_variables_json"; + +NSString *CT_PE_DEFINE_VARS_ENDPOINT = @"defineVars"; + +NSString *CT_PE_VARS_PAYLOAD_TYPE = @"varsPayload"; +NSString *CT_PE_VARS_PAYLOAD_KEY = @"vars"; +NSString *CT_PE_VAR_TYPE = @"type"; +NSString *CT_PE_NUMBER_TYPE = @"number"; +NSString *CT_PE_BOOL_TYPE = @"boolean"; +NSString *CT_PE_DEFAULT_VALUE = @"defaultValue"; diff --git a/CleverTapSDK/CTDeviceInfo.h b/CleverTapSDK/CTDeviceInfo.h index 54983546..ebcfc766 100644 --- a/CleverTapSDK/CTDeviceInfo.h +++ b/CleverTapSDK/CTDeviceInfo.h @@ -25,7 +25,6 @@ @property (atomic, readwrite) NSString *library; @property (assign, readonly) BOOL wifi; @property (strong, readonly) NSMutableArray* validationErrors; -@property (strong, readonly) NSString *signedCallSDKVersion; - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config andCleverTapID:(NSString *)cleverTapID; - (void)forceUpdateDeviceID:(NSString *)newDeviceID; @@ -34,5 +33,4 @@ - (BOOL)isErrorDeviceID; - (void)incrementLocalInAppCount; - (int)getLocalInAppCount; -- (void)setSignedCallSDKVersion: (NSString *)version; @end diff --git a/CleverTapSDK/CTDeviceInfo.m b/CleverTapSDK/CTDeviceInfo.m index 67205939..988db20a 100644 --- a/CleverTapSDK/CTDeviceInfo.m +++ b/CleverTapSDK/CTDeviceInfo.m @@ -40,7 +40,6 @@ static NSString *_radio; static NSString *_deviceWidth; static NSString *_deviceHeight; -static NSString *_signedCallSDKVersion; #if !CLEVERTAP_NO_REACHABILITY_SUPPORT SCNetworkReachabilityRef _reachability; @@ -472,14 +471,6 @@ - (NSString *)getCurrentRadioAccessTechnology { } #endif -- (void)setSignedCallSDKVersion: (NSString *)version { - _signedCallSDKVersion = version; -} - -- (NSString *)signedCallSDKVersion { - return _signedCallSDKVersion; -} - - (void)incrementLocalInAppCount { self.localInAppCount = self.localInAppCount + 1; [CTPreferences putInt:self.localInAppCount forKey:kCLTAP_LOCAL_INAPP_COUNT]; diff --git a/CleverTapSDK/CTDomainFactory.h b/CleverTapSDK/CTDomainFactory.h new file mode 100644 index 00000000..40800956 --- /dev/null +++ b/CleverTapSDK/CTDomainFactory.h @@ -0,0 +1,31 @@ +// +// CTDomainFactory.h +// CleverTapSDK +// +// Created by Akash Malhotra on 19/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CleverTapInstanceConfig.h" +#if CLEVERTAP_SSL_PINNING +#import "CTPinnedNSURLSessionDelegate.h" +#endif + + +@interface CTDomainFactory : NSObject +@property (nonatomic, strong, nullable) NSString *redirectDomain; +@property (nonatomic, strong, nullable) NSString *explictEndpointDomain; +@property (nonatomic, strong, nullable) NSString *redirectNotifViewedDomain; +@property (nonatomic, strong, nullable) NSString *explictNotifViewedEndpointDomain; + +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig* _Nonnull)config; +- (void)persistRedirectDomain; +- (void)persistRedirectNotifViewedDomain; +- (void)clearRedirectDomain; + +#if CLEVERTAP_SSL_PINNING +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig* _Nonnull)config pinnedNSURLSessionDelegate: (CTPinnedNSURLSessionDelegate* _Nonnull)pinnedNSURLSessionDelegate sslCertNames:(NSArray* _Nonnull)sslCertNames; +#endif +@end + diff --git a/CleverTapSDK/CTDomainFactory.m b/CleverTapSDK/CTDomainFactory.m new file mode 100644 index 00000000..64a3615f --- /dev/null +++ b/CleverTapSDK/CTDomainFactory.m @@ -0,0 +1,137 @@ +// +// CTDomainFactory.m +// CleverTapSDK +// +// Created by Akash Malhotra on 19/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTDomainFactory.h" +#import "CTPreferences.h" +#import "CTConstants.h" +#import "CleverTapInstanceConfigPrivate.h" + + +NSString *const REDIRECT_DOMAIN_KEY = @"CLTAP_REDIRECT_DOMAIN_KEY"; +NSString *const REDIRECT_NOTIF_VIEWED_DOMAIN_KEY = @"CLTAP_REDIRECT_NOTIF_VIEWED_DOMAIN_KEY"; + +@interface CTDomainFactory () +@property (nonatomic, strong) CleverTapInstanceConfig *config; + +#if CLEVERTAP_SSL_PINNING +@property(nonatomic, strong) CTPinnedNSURLSessionDelegate *urlSessionDelegate; +@property (nonatomic, strong) NSArray *sslCertNames; +#endif +@end + +@implementation CTDomainFactory + +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig* _Nonnull)config { + self = [super init]; + if (self) { + self.config = config; + self.redirectDomain = [self loadRedirectDomain]; + self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; + } + return self; +} + +#if CLEVERTAP_SSL_PINNING +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig* _Nonnull)config pinnedNSURLSessionDelegate: (CTPinnedNSURLSessionDelegate* _Nonnull)pinnedNSURLSessionDelegate sslCertNames:(NSArray* _Nonnull)sslCertNames{ + self = [super init]; + if (self) { + self.config = config; + self.urlSessionDelegate = pinnedNSURLSessionDelegate; + self.sslCertNames = sslCertNames; + self.redirectDomain = [self loadRedirectDomain]; + self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; + } + return self; +} +#endif + +- (void)clearRedirectDomain { + self.redirectDomain = nil; + self.redirectNotifViewedDomain = nil; + [self persistRedirectDomain]; // if nil persist will remove + self.redirectDomain = [self loadRedirectDomain]; // reload explicit domain if we have one else will be nil + self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; // reload explicit notification viewe domain if we have one else will be nil +} + +- (NSString *)loadRedirectDomain { + NSString *region = self.config.accountRegion; + if (region) { + region = [region stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; + if (region.length > 0) { + self.explictEndpointDomain = [NSString stringWithFormat:@"%@.%@", region, kCTApiDomain]; + return self.explictEndpointDomain; + } + } + NSString *proxyDomain = self.config.proxyDomain; + if (proxyDomain) { + proxyDomain = [proxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; + if (proxyDomain.length > 0) { + self.explictEndpointDomain = proxyDomain; + return self.explictEndpointDomain; + } + } + NSString *domain = nil; + if (self.config.isDefaultInstance) { + domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_DOMAIN_KEY config: self.config] withResetValue:[CTPreferences getStringForKey:REDIRECT_DOMAIN_KEY withResetValue:nil]]; + } else { + domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_DOMAIN_KEY config: self.config] withResetValue:nil]; + } + return domain; +} + +- (NSString *)loadRedirectNotifViewedDomain { + NSString *region = self.config.accountRegion; + if (region) { + region = [region stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; + if (region.length > 0) { + self.explictNotifViewedEndpointDomain = [NSString stringWithFormat:@"%@-%@", region, kCTNotifViewedApiDomain]; + return self.explictNotifViewedEndpointDomain; + } + } + NSString *spikyProxyDomain = self.config.spikyProxyDomain; + if (spikyProxyDomain) { + spikyProxyDomain = [spikyProxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; + if (spikyProxyDomain.length > 0) { + self.explictNotifViewedEndpointDomain = spikyProxyDomain; + return self.explictNotifViewedEndpointDomain; + } + } + NSString *domain = nil; + if (self.config.isDefaultInstance) { + domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config] withResetValue:[CTPreferences getStringForKey:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY withResetValue:nil]]; + } else { + domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config] withResetValue:nil]; + } + return domain; +} + +- (void)persistRedirectDomain { + if (self.redirectDomain != nil) { + [CTPreferences putString:self.redirectDomain forKey:[CTPreferences storageKeyWithSuffix:REDIRECT_DOMAIN_KEY config: self.config]]; +#if CLEVERTAP_SSL_PINNING + [self.urlSessionDelegate pinSSLCerts:self.sslCertNames forDomains:@[kCTApiDomain, self.redirectDomain]]; +#endif + } else { + [CTPreferences removeObjectForKey:REDIRECT_DOMAIN_KEY]; + [CTPreferences removeObjectForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_DOMAIN_KEY config: self.config]]; + } +} + +- (void)persistRedirectNotifViewedDomain { + if (self.redirectNotifViewedDomain != nil) { + [CTPreferences putString:self.redirectNotifViewedDomain forKey:[CTPreferences storageKeyWithSuffix:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config]]; +#if CLEVERTAP_SSL_PINNING + [self.urlSessionDelegate pinSSLCerts:self.sslCertNames forDomains:@[kCTNotifViewedApiDomain, self.redirectNotifViewedDomain]]; +#endif + } else { + [CTPreferences removeObjectForKey:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY]; + [CTPreferences removeObjectForKey:[CTPreferences storageKeyWithSuffix:REDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config]]; + } +} + +@end diff --git a/CleverTapSDK/CTLogger.h b/CleverTapSDK/CTLogger.h index 5cc2b120..b8a8160b 100644 --- a/CleverTapSDK/CTLogger.h +++ b/CleverTapSDK/CTLogger.h @@ -4,5 +4,5 @@ + (void)setDebugLevel:(int)level; + (int)getDebugLevel; - ++ (void)logInternalError:(NSException *)e; @end diff --git a/CleverTapSDK/CTLogger.m b/CleverTapSDK/CTLogger.m index 29ee6547..74f8a713 100644 --- a/CleverTapSDK/CTLogger.m +++ b/CleverTapSDK/CTLogger.m @@ -1,4 +1,5 @@ #import "CTLogger.h" +#import "CTConstants.h" @implementation CTLogger @@ -12,4 +13,8 @@ + (int)getDebugLevel { return _debugLevel; } ++ (void)logInternalError:(NSException *)e { + CleverTapLogDebug(_debugLevel, @"%@: Caught exception in code: %@\n%@", self, e, [e callStackSymbols]); +} + @end diff --git a/CleverTapSDK/CTPreferences.h b/CleverTapSDK/CTPreferences.h index f0a94cdf..2e49042b 100644 --- a/CleverTapSDK/CTPreferences.h +++ b/CleverTapSDK/CTPreferences.h @@ -23,6 +23,8 @@ + (BOOL)archiveObject:(id _Nonnull)object forFileName:(NSString *_Nonnull)fileName; -+ (NSString * _Nonnull)storageKeyWithSuffix: (NSString * _Nonnull)suffix config: (CleverTapInstanceConfig* _Nonnull)config; ++ (NSString *_Nonnull)storageKeyWithSuffix: (NSString *_Nonnull)suffix config: (CleverTapInstanceConfig *_Nonnull)config; + ++ (NSString *_Nonnull)filePathfromFileName:(NSString *_Nonnull)filename; @end diff --git a/CleverTapSDK/CTPreferences.m b/CleverTapSDK/CTPreferences.m index 42daa2b6..1e27d4e5 100644 --- a/CleverTapSDK/CTPreferences.m +++ b/CleverTapSDK/CTPreferences.m @@ -161,7 +161,7 @@ + (BOOL)archiveObject:(id)object forFileName:(NSString *)filename { if (@available(iOS 11.0, tvOS 11.0, *)) { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object requiringSecureCoding:NO error:&archiveError]; - [data writeToFile:filePath options:NSDataWritingAtomic error:&writeError]; + success = [data writeToFile:filePath options:NSDataWritingAtomic error:&writeError]; if (archiveError) { CleverTapLogStaticInternal(@"%@ failed to archive data at %@: %@", self, filePath, archiveError); } diff --git a/CleverTapSDK/CTRequest.h b/CleverTapSDK/CTRequest.h new file mode 100644 index 00000000..b03f0b27 --- /dev/null +++ b/CleverTapSDK/CTRequest.h @@ -0,0 +1,27 @@ +// +// CTRequest.h +// CleverTapSDK +// +// Created by Akash Malhotra on 09/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CleverTapInstanceConfig.h" + +typedef void (^CTNetworkResponseBlock)(NSData * _Nullable data, NSURLResponse *_Nullable response); +typedef void (^CTNetworkResponseErrorBlock)(NSError * _Nullable error); + +@interface CTRequest : NSObject + +- (CTRequest *_Nonnull)initWithHttpMethod:(NSString *_Nonnull)httpMethod config:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url; + +- (void)onResponse:(CTNetworkResponseBlock _Nonnull)responseBlock; +- (void)onError:(CTNetworkResponseErrorBlock _Nonnull)errorBlock; + +@property (nonatomic, strong, nonnull) NSMutableURLRequest *urlRequest; +@property (nonatomic, strong, nonnull) CTNetworkResponseBlock responseBlock; +@property (nonatomic, strong, nullable) CTNetworkResponseErrorBlock errorBlock; + +@end + diff --git a/CleverTapSDK/CTRequest.m b/CleverTapSDK/CTRequest.m new file mode 100644 index 00000000..b8734910 --- /dev/null +++ b/CleverTapSDK/CTRequest.m @@ -0,0 +1,66 @@ +// +// CTRequest.m +// CleverTapSDK +// +// Created by Akash Malhotra on 09/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTRequest.h" +#import "CTConstants.h" +#import "CTUtils.h" + +NSString *const ACCOUNT_ID_HEADER = @"X-CleverTap-Account-Id"; +NSString *const ACCOUNT_TOKEN_HEADER = @"X-CleverTap-Token"; + +@interface CTRequest() + +@property (nonatomic, strong, nullable) id params; +@property (nonatomic, strong) NSString *httpMethod; +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) NSString *url; + + +@end + +@implementation CTRequest + +- (CTRequest *_Nonnull)initWithHttpMethod:(NSString *_Nonnull)httpMethod config:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url { + self = [super init]; + if (self) { + _httpMethod = httpMethod; + _params = params; + _config = config; + _url = url; + _urlRequest = [self createURLRequest]; + } + return self; +} + +- (NSMutableURLRequest *)createURLRequest { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:_url]]; + NSString *accountId = self.config.accountId; + NSString *accountToken = self.config.accountToken; + if (accountId) { + [request setValue:accountId forHTTPHeaderField:ACCOUNT_ID_HEADER]; + } + if (accountToken) { + [request setValue:accountToken forHTTPHeaderField:ACCOUNT_TOKEN_HEADER]; + } + if ([_httpMethod isEqualToString:@"POST"] && _params > 0) { + NSString *jsonBody = [CTUtils jsonObjectToString:_params]; + request.HTTPBody = [jsonBody dataUsingEncoding:NSUTF8StringEncoding]; + request.HTTPMethod = @"POST"; + } + return request; +} + +- (void)onResponse:(CTNetworkResponseBlock _Nonnull)responseBlock { + _responseBlock = responseBlock; +} + +- (void)onError:(CTNetworkResponseErrorBlock _Nonnull)errorBlock { + _errorBlock = errorBlock; +} + +@end diff --git a/CleverTapSDK/CTRequestFactory.h b/CleverTapSDK/CTRequestFactory.h new file mode 100644 index 00000000..9586ba7b --- /dev/null +++ b/CleverTapSDK/CTRequestFactory.h @@ -0,0 +1,20 @@ +// +// CTRequestFactory.h +// CleverTapSDK +// +// Created by Akash Malhotra on 09/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTRequest.h" +#import "CleverTapInstanceConfig.h" + +@interface CTRequestFactory : NSObject + ++ (CTRequest *_Nonnull)helloRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config; ++ (CTRequest *_Nonnull)eventRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url; ++ (CTRequest *_Nonnull)syncVarsRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url; +@end + + diff --git a/CleverTapSDK/CTRequestFactory.m b/CleverTapSDK/CTRequestFactory.m new file mode 100644 index 00000000..5c2923c5 --- /dev/null +++ b/CleverTapSDK/CTRequestFactory.m @@ -0,0 +1,26 @@ +// +// CTRequestFactory.m +// CleverTapSDK +// +// Created by Akash Malhotra on 09/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTRequestFactory.h" +#import "CTConstants.h" + +@implementation CTRequestFactory + ++ (CTRequest *_Nonnull)helloRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config { + return [[CTRequest alloc]initWithHttpMethod:@"GET" config:config params:nil url:kHANDSHAKE_URL]; +} + ++ (CTRequest *_Nonnull)eventRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url { + return [[CTRequest alloc]initWithHttpMethod:@"POST" config:config params: params url:url]; +} + ++ (CTRequest *_Nonnull)syncVarsRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params url:(NSString *_Nonnull)url { + return [[CTRequest alloc]initWithHttpMethod:@"POST" config:config params: params url:url]; +} + +@end diff --git a/CleverTapSDK/CTRequestSender.h b/CleverTapSDK/CTRequestSender.h new file mode 100644 index 00000000..62c66ee3 --- /dev/null +++ b/CleverTapSDK/CTRequestSender.h @@ -0,0 +1,23 @@ +// +// CTRequestSender.h +// CleverTapSDK +// +// Created by Akash Malhotra on 11/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTRequest.h" +#if CLEVERTAP_SSL_PINNING +#import "CTPinnedNSURLSessionDelegate.h" +#endif + +@interface CTRequestSender : NSObject +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig *_Nonnull)config redirectDomain:(NSString* _Nonnull)redirectDomain; +- (void)send:(CTRequest *_Nonnull)ctRequest; + +#if CLEVERTAP_SSL_PINNING +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig *_Nonnull)config redirectDomain:(NSString* _Nonnull)redirectDomain pinnedNSURLSessionDelegate: (CTPinnedNSURLSessionDelegate* _Nonnull)pinnedNSURLSessionDelegate sslCertNames:(NSArray* _Nonnull)sslCertNames; +#endif +@end + diff --git a/CleverTapSDK/CTRequestSender.m b/CleverTapSDK/CTRequestSender.m new file mode 100644 index 00000000..352eb9bd --- /dev/null +++ b/CleverTapSDK/CTRequestSender.m @@ -0,0 +1,99 @@ +// +// CTRequestSender.m +// CleverTapSDK +// +// Created by Akash Malhotra on 11/01/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTRequestSender.h" +#import "CTConstants.h" + +#if CLEVERTAP_SSL_PINNING +#import "CTPinnedNSURLSessionDelegate.h" +#endif + +@interface CTRequestSender () +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) NSURLSession *urlSession; +@property (nonatomic, strong) NSString *redirectDomain; +@property (nonatomic, assign, readonly) BOOL sslPinningEnabled; + +#if CLEVERTAP_SSL_PINNING +@property(nonatomic, strong) CTPinnedNSURLSessionDelegate *urlSessionDelegate; +@property (nonatomic, strong) NSArray *sslCertNames; +#endif +@end + +@implementation CTRequestSender + +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig *_Nonnull)config redirectDomain:(NSString* _Nonnull)redirectDomain { + + if ((self = [super init])) { + self.config = config; + self.redirectDomain = redirectDomain; + [self setUpUrlSession]; + + } + return self; +} + +#if CLEVERTAP_SSL_PINNING +- (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig *_Nonnull)config redirectDomain:(NSString* _Nonnull)redirectDomain pinnedNSURLSessionDelegate: (CTPinnedNSURLSessionDelegate* _Nonnull)pinnedNSURLSessionDelegate sslCertNames:(NSArray* _Nonnull)sslCertNames { + if ((self = [super init])) { + self.config = config; + self.urlSessionDelegate = pinnedNSURLSessionDelegate; + self.sslCertNames = sslCertNames; + self.redirectDomain = redirectDomain; + [self setUpUrlSession]; + + } + return self; +} +#endif + +- (void)setUpUrlSession { + if (!_urlSession) { + NSURLSessionConfiguration *sc = [NSURLSessionConfiguration defaultSessionConfiguration]; + [sc setHTTPAdditionalHeaders:@{ + @"Content-Type" : @"application/json; charset=utf-8" + }]; + + sc.timeoutIntervalForRequest = CLTAP_REQUEST_TIME_OUT_INTERVAL; + sc.timeoutIntervalForResource = CLTAP_REQUEST_TIME_OUT_INTERVAL; + [sc setHTTPShouldSetCookies:NO]; + [sc setRequestCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + +#if CLEVERTAP_SSL_PINNING + _sslPinningEnabled = YES; + self.urlSessionDelegate = [[CTPinnedNSURLSessionDelegate alloc] initWithConfig:self.config]; + NSMutableArray *domains = [NSMutableArray arrayWithObjects:kCTApiDomain, nil]; + if (self.redirectDomain && ![self.redirectDomain isEqualToString:kCTApiDomain]) { + [domains addObject:self.redirectDomain]; + } + // WITH SSL PINNING ENABLED AND REGION NOT SPECIFIED BY THE USER, WE WILL DEFAULT TO EU1 AND PIN THE CERT TO EU1 + else if (!self.redirectDomain) { + [domains addObject:[NSString stringWithFormat:@"eu1.%@", kCTApiDomain]]; + } + [self.urlSessionDelegate pinSSLCerts:_sslCertNames forDomains:domains]; + self.urlSession = [NSURLSession sessionWithConfiguration:sc delegate:self.urlSessionDelegate delegateQueue:nil]; +#else + _sslPinningEnabled = NO; + _urlSession = [NSURLSession sessionWithConfiguration:sc]; +#endif + } +} + +- (void)send:(CTRequest *_Nonnull)ctRequest { + NSURLSessionDataTask *task = [_urlSession + dataTaskWithRequest:ctRequest.urlRequest + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + ctRequest.errorBlock(error); + } + ctRequest.responseBlock(data, response); + }]; + [task resume]; +} + +@end diff --git a/CleverTapSDK/CTUtils.h b/CleverTapSDK/CTUtils.h index 7257855a..93656cef 100644 --- a/CleverTapSDK/CTUtils.h +++ b/CleverTapSDK/CTUtils.h @@ -7,5 +7,6 @@ + (BOOL)doesString:(NSString *)s startWith:(NSString *)prefix; + (NSString *)deviceTokenStringFromData:(NSData *)tokenData; + (double)toTwoPlaces:(double)x; - ++ (BOOL)isNullOrEmpty:(id)obj; ++ (NSString *)jsonObjectToString:(id)object; @end diff --git a/CleverTapSDK/CTUtils.m b/CleverTapSDK/CTUtils.m index 3ce9ba22..1aa74c54 100644 --- a/CleverTapSDK/CTUtils.m +++ b/CleverTapSDK/CTUtils.m @@ -80,4 +80,34 @@ + (double)toTwoPlaces:(double)x { return result; } ++ (BOOL)isNullOrEmpty:(id)obj +{ + // Need to check for NSString to support RubyMotion. + // Ruby String respondsToSelector(count) is true for count: in RubyMotion + return obj == nil + || ([obj respondsToSelector:@selector(length)] && [obj length] == 0) + || ([obj respondsToSelector:@selector(count)] + && ![obj isKindOfClass:[NSString class]] && [obj count] == 0); +} + ++ (NSString *)jsonObjectToString:(id)object { + if ([object isKindOfClass:[NSString class]]) { + return object; + } + @try { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:object + options:0 + error:&error]; + if (error) { + return @""; + } + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + return jsonString; + } + @catch (NSException *exception) { + return @""; + } +} + @end diff --git a/CleverTapSDK/CleverTap+CTVar.h b/CleverTapSDK/CleverTap+CTVar.h new file mode 100644 index 00000000..acc9d89f --- /dev/null +++ b/CleverTapSDK/CleverTap+CTVar.h @@ -0,0 +1,58 @@ +// +// CleverTap+CTVar.h +// CleverTapSDK +// +// Created by Akash Malhotra on 18/02/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CleverTap.h" +@class CTVar; + +NS_ASSUME_NONNULL_BEGIN + +@interface CleverTap (Vars) + +- (CTVar *)defineVar:(NSString *)name +NS_SWIFT_NAME(defineVar(name:)); +- (CTVar *)defineVar:(NSString *)name withInt:(int)defaultValue +NS_SWIFT_NAME(defineVar(name:integer:)); +- (CTVar *)defineVar:(NSString *)name withFloat:(float)defaultValue +NS_SWIFT_NAME(defineVar(name:float:)); +- (CTVar *)defineVar:(NSString *)name withDouble:(double)defaultValue +NS_SWIFT_NAME(defineVar(name:double:)); +- (CTVar *)defineVar:(NSString *)name withCGFloat:(CGFloat)cgFloatValue +NS_SWIFT_NAME(defineVar(name:cgFloat:)); +- (CTVar *)defineVar:(NSString *)name withShort:(short)defaultValue +NS_SWIFT_NAME(defineVar(name:short:)); +- (CTVar *)defineVar:(NSString *)name withBool:(BOOL)defaultValue +NS_SWIFT_NAME(defineVar(name:boolean:)); +- (CTVar *)defineVar:(NSString *)name withString:(nullable NSString *)defaultValue +NS_SWIFT_NAME(defineVar(name:string:)); +- (CTVar *)defineVar:(NSString *)name withNumber:(nullable NSNumber *)defaultValue +NS_SWIFT_NAME(defineVar(name:number:)); +- (CTVar *)defineVar:(NSString *)name withInteger:(NSInteger)defaultValue +NS_SWIFT_NAME(defineVar(name:NSInteger:)); +- (CTVar *)defineVar:(NSString *)name withLong:(long)defaultValue +NS_SWIFT_NAME(defineVar(name:long:)); +- (CTVar *)defineVar:(NSString *)name withLongLong:(long long)defaultValue +NS_SWIFT_NAME(defineVar(name:longLong:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedChar:(unsigned char)defaultValue +NS_SWIFT_NAME(defineVar(name:unsignedChar:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedInt:(unsigned int)defaultValue +NS_SWIFT_NAME(defineVar(name:unsignedInt:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedInteger:(NSUInteger)defaultValue +NS_SWIFT_NAME(defineVar(name:unsignedInteger:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedLong:(unsigned long)defaultValue +NS_SWIFT_NAME(defineVar(name:unsignedLong:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedLongLong:(unsigned long long)defaultValue +NS_SWIFT_NAME(defineVar(name:unsignedLongLong:)); +- (CTVar *)defineVar:(NSString *)name withUnsignedShort:(unsigned short)defaultValue +NS_SWIFT_NAME(defineVar(name:UnsignedShort:)); +- (CTVar *)defineVar:(NSString *)name withDictionary:(nullable NSDictionary *)defaultValue +NS_SWIFT_NAME(defineVar(name:dictionary:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/CleverTap+FeatureFlags.h b/CleverTapSDK/CleverTap+FeatureFlags.h index c1b1597d..cf25a192 100644 --- a/CleverTapSDK/CleverTap+FeatureFlags.h +++ b/CleverTapSDK/CleverTap+FeatureFlags.h @@ -1,19 +1,24 @@ #import #import "CleverTap.h" +__attribute__((deprecated("This protocol has been deprecated and will be removed in the future versions of this SDK."))) @protocol CleverTapFeatureFlagsDelegate @optional -- (void)ctFeatureFlagsUpdated; +- (void)ctFeatureFlagsUpdated +__attribute__((deprecated("This protocol method has been deprecated and will be removed in the future versions of this SDK."))); @end @interface CleverTap (FeatureFlags) -@property (atomic, strong, readonly, nonnull) CleverTapFeatureFlags *featureFlags; +@property (atomic, strong, readonly, nonnull) CleverTapFeatureFlags *featureFlags +__attribute__((deprecated("This property has been deprecated and will be removed in the future versions of this SDK.")));; @end @interface CleverTapFeatureFlags : NSObject -@property (nonatomic, weak) id _Nullable delegate; +@property (nonatomic, weak) id _Nullable delegate +__attribute__((deprecated("This property has been deprecated and will be removed in the future versions of this SDK.")));; -- (BOOL)get:(NSString* _Nonnull)key withDefaultValue:(BOOL)defaultValue; +- (BOOL)get:(NSString* _Nonnull)key withDefaultValue:(BOOL)defaultValue +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK.")));; @end diff --git a/CleverTapSDK/CleverTap+ProductConfig.h b/CleverTapSDK/CleverTap+ProductConfig.h index bab09bcc..b60e476b 100644 --- a/CleverTapSDK/CleverTap+ProductConfig.h +++ b/CleverTapSDK/CleverTap+ProductConfig.h @@ -1,11 +1,15 @@ #import #import "CleverTap.h" +__attribute__((deprecated("This protocol has been deprecated and will be removed in the future versions of this SDK."))) @protocol CleverTapProductConfigDelegate @optional -- (void)ctProductConfigFetched; -- (void)ctProductConfigActivated; -- (void)ctProductConfigInitialized; +- (void)ctProductConfigFetched +__attribute__((deprecated("This protocol method has been deprecated and will be removed in the future versions of this SDK."))); +- (void)ctProductConfigActivated +__attribute__((deprecated("This protocol method has been deprecated and will be removed in the future versions of this SDK."))); +- (void)ctProductConfigInitialized +__attribute__((deprecated("This protocol method has been deprecated and will be removed in the future versions of this SDK."))); @end @interface CleverTap(ProductConfig) @@ -32,7 +36,8 @@ @interface CleverTapProductConfig : NSObject -@property (nonatomic, weak) id _Nullable delegate; +@property (nonatomic, weak) id _Nullable delegate +__attribute__((deprecated("This property has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -41,7 +46,8 @@ Fetches product configs, adhering to the default minimum fetch interval. */ -- (void)fetch; +- (void)fetch +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -50,7 +56,8 @@ Fetches product configs, adhering to the specified minimum fetch interval in seconds. */ -- (void)fetchWithMinimumInterval:(NSTimeInterval)minimumInterval; +- (void)fetchWithMinimumInterval:(NSTimeInterval)minimumInterval +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -59,7 +66,8 @@ Sets the minimum interval between successive fetch calls. */ -- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval; +- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -68,7 +76,8 @@ Activates Fetched Config data to the Active Config, so that the fetched key value pairs take effect. */ -- (void)activate; +- (void)activate +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -77,7 +86,8 @@ Fetches and then activates the fetched product configs. */ -- (void)fetchAndActivate; +- (void)fetchAndActivate +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -86,7 +96,8 @@ Sets default configs using the given Dictionary */ -- (void)setDefaults:(NSDictionary *_Nullable)defaults; +- (void)setDefaults:(NSDictionary *_Nullable)defaults +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -95,7 +106,8 @@ Sets default configs using the given plist */ -- (void)setDefaultsFromPlistFileName:(NSString *_Nullable)fileName; +- (void)setDefaultsFromPlistFileName:(NSString *_Nullable)fileName +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -104,7 +116,8 @@ Returns the config value of the given key */ -- (CleverTapConfigValue *_Nullable)get:(NSString* _Nonnull)key; +- (CleverTapConfigValue *_Nullable)get:(NSString* _Nonnull)key +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -113,7 +126,8 @@ Returns the last fetch timestamp */ -- (NSDate *_Nullable)getLastFetchTimeStamp; +- (NSDate *_Nullable)getLastFetchTimeStamp +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); /*! @method @@ -122,7 +136,8 @@ Deletes all activated, fetched and defaults configs and resets all Product Config settings. */ -- (void)reset; +- (void)reset +__attribute__((deprecated("This method has been deprecated and will be removed in the future versions of this SDK."))); @end diff --git a/CleverTapSDK/CleverTap.h b/CleverTapSDK/CleverTap.h index 66517606..405b388c 100644 --- a/CleverTapSDK/CleverTap.h +++ b/CleverTapSDK/CleverTap.h @@ -31,6 +31,7 @@ @class CleverTapInstanceConfig; @class CleverTapFeatureFlags; @class CleverTapProductConfig; +#import "CTVar.h" #pragma clang diagnostic push #pragma ide diagnostic ignored "OCUnusedMethodInspection" @@ -1227,6 +1228,17 @@ extern NSString * _Nonnull const CleverTapProfileDidInitializeNotification; */ - (void)setLibrary:(NSString * _Nonnull)name; +/*! + @method + + @abstract + Set the Library name and version for Auxiliary SDKs + + @discussion + Call this to method to set library name and version in the Auxiliary SDK + */ +- (void)setCustomSdkVersion:(NSString * _Nonnull)name version:(int)version; + /*! @method @@ -1287,16 +1299,6 @@ extern NSString * _Nonnull const CleverTapProfileDidInitializeNotification; */ - (void)recordSignedCallEvent:(int)eventRawValue forCallDetails:(NSDictionary *_Nonnull)calldetails; -/*! - @method - - @abstract - Record Signed Call SDK version. - - @param version Signed call SDK version - */ -- (void)setSignedCallVersion:(NSString* _Nullable)version; - /*! @method @@ -1328,6 +1330,85 @@ extern NSString * _Nonnull const CleverTapProfileDidInitializeNotification; */ + (BOOL)isValidCleverTapId:(NSString *_Nullable)cleverTapID; +#pragma mark Product Experiences - Vars + +/*! + @method + + @abstract + Adds a callback to be invoked when variables are initialised with server values. Will be called each time new values are fetched. + + @param block a callback to add. + */ +- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block; + +/*! + @method + + @abstract + Adds a callback to be invoked only once when variables are initialised with server values. + + @param block a callback to add. + */ +- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block; + +/*! + @method + + @abstract + Uploads variables to the server. Requires Development/Debug build/configuration. + */ +- (void)syncVariables; + +/*! + @method + + @abstract + Uploads variables to the server. + + @param isProduction Provide `true` if variables must be sync in Productuon build/configuration. + */ +- (void)syncVariables:(BOOL)isProduction; + +/*! + @method + + @abstract + Forces variables to update from the server. + + @discussion + Forces variables to update from the server. If variables have changed, the appropriate callbacks will fire. Use sparingly as if the app is updated, you'll have to deal with potentially inconsistent state or user experience. + The provided callback has a boolean flag whether the update was successful or not. The callback fires regardless + of whether the variables have changed. + + @param block a callback with a boolean flag whether the update was successful. + */ +- (void)fetchVariables:(CleverTapFetchVariablesBlock _Nullable)block; + +/*! + @method + + @abstract + Get an instance of a variable or a group. + + @param name The name of the variable or the group. + + @return + The instance of the variable or the group, or nil if not created yet. + + */ +- (CTVar * _Nullable)getVariable:(NSString * _Nonnull)name; + +/*! + @method + + @abstract + Get a copy of the current value of a variable or a group. + + @param name The name of the variable or the group. + */ +- (id _Nullable)getVariableValue:(NSString * _Nonnull)name; + @end #pragma clang diagnostic pop diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index 3158830f..6c8044e3 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -72,12 +72,21 @@ #import "CleverTapProductConfigPrivate.h" #import "CTProductConfigController.h" +#import "CTVarCache.h" +#import "CTVariables.h" +#import "CleverTap+CTVar.h" + +#import "CTRequestFactory.h" +#import "CTRequestSender.h" +#import "CTDomainFactory.h" #import "CleverTap+SCDomain.h" + #import static const void *const kQueueKey = &kQueueKey; static const void *const kNotificationQueueKey = &kNotificationQueueKey; static BOOL isLocationEnabled; +static NSMutableDictionary *auxiliarySdkVersions; static NSRecursiveLock *instanceLock; static const int kMaxBatchSize = 49; @@ -85,8 +94,6 @@ NSString *const kQUEUE_NAME_EVENTS = @"events"; NSString *const kQUEUE_NAME_NOTIFICATIONS = @"notifications"; -NSString *const kHANDSHAKE_URL = @"https://eu1.clevertap-prod.com/hello"; - NSString *const kREDIRECT_DOMAIN_KEY = @"CLTAP_REDIRECT_DOMAIN_KEY"; NSString *const kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY = @"CLTAP_REDIRECT_NOTIF_VIEWED_DOMAIN_KEY"; NSString *const kMUTED_TS_KEY = @"CLTAP_MUTED_TS_KEY"; @@ -95,8 +102,6 @@ NSString *const kREDIRECT_NOTIF_VIEWED_HEADER = @"X-WZRK-SPIKY-RD"; NSString *const kMUTE_HEADER = @"X-WZRK-MUTE"; -NSString *const kACCOUNT_ID_HEADER = @"X-CleverTap-Account-Id"; -NSString *const kACCOUNT_TOKEN_HEADER = @"X-CleverTap-Token"; NSString *const kI_KEY = @"CLTAP_I_KEY"; NSString *const kJ_KEY = @"CLTAP_J_KEY"; @@ -202,10 +207,8 @@ @interface CleverTap () { @property (nonatomic, strong) NSMutableArray *profileQueue; @property (nonatomic, strong) NSMutableArray *notificationsQueue; @property (nonatomic, strong) NSURLSession *urlSession; -@property (nonatomic, strong) NSString *redirectDomain; -@property (nonatomic, strong) NSString *explictEndpointDomain; -@property (nonatomic, strong) NSString *redirectNotifViewedDomain; -@property (nonatomic, strong) NSString *explictNotifViewedEndpointDomain; +@property (nonatomic, strong) CTDomainFactory *domainFactory; +@property (nonatomic, strong) CTRequestSender *requestSender; @property (nonatomic, assign) NSTimeInterval lastMutedTs; @property (nonatomic, assign) int sendQueueFails; @@ -239,7 +242,7 @@ @interface CleverTap () { @property (atomic, weak) id urlDelegate; @property (atomic, weak) id pushNotificationDelegate; @property (atomic, weak) id inAppNotificationDelegate; -@property (atomic, weak) id domainDelegate; +@property (nonatomic, weak) id domainDelegate; #if !CLEVERTAP_NO_INAPP_SUPPORT @property (atomic, weak) id pushPermissionDelegate; #endif @@ -251,6 +254,8 @@ @interface CleverTap () { @property (atomic, assign) BOOL geofenceLocation; @property (nonatomic, strong) NSString *gfSDKVersion; +@property (nonatomic, strong) CTVariables *variables; + - (instancetype)init __unavailable; @end @@ -685,6 +690,9 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( [self _initProductConfig]; + // Initialise Variables + self.variables = [[CTVariables alloc] initWithConfig:self.config deviceInfo:self.deviceInfo]; + [self notifyUserProfileInitialized]; } @@ -803,42 +811,17 @@ - (void)initNetworking { } else { self.lastMutedTs = [CTPreferences getIntForKey:[CTPreferences storageKeyWithSuffix:kLAST_TS_KEY config: self.config] withResetValue:0]; } - self.redirectDomain = [self loadRedirectDomain]; - self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; - [self setUpUrlSession]; - [self doHandshakeAsync]; -} -- (void)setUpUrlSession { - if (!self.urlSession) { - NSURLSessionConfiguration *sc = [NSURLSessionConfiguration defaultSessionConfiguration]; - [sc setHTTPAdditionalHeaders:@{ - @"Content-Type" : @"application/json; charset=utf-8" - }]; - - sc.timeoutIntervalForRequest = CLTAP_REQUEST_TIME_OUT_INTERVAL; - sc.timeoutIntervalForResource = CLTAP_REQUEST_TIME_OUT_INTERVAL; - [sc setHTTPShouldSetCookies:NO]; - [sc setRequestCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; - #if CLEVERTAP_SSL_PINNING - _sslPinningEnabled = YES; - self.urlSessionDelegate = [[CTPinnedNSURLSessionDelegate alloc] initWithConfig:self.config]; - NSMutableArray *domains = [NSMutableArray arrayWithObjects:kCTApiDomain, nil]; - if (self.redirectDomain && ![self.redirectDomain isEqualToString:kCTApiDomain]) { - [domains addObject:self.redirectDomain]; - } - // WITH SSL PINNING ENABLED AND REGION NOT SPECIFIED BY THE USER, WE WILL DEFAULT TO EU1 AND PIN THE CERT TO EU1 - else if (!self.redirectDomain) { - [domains addObject:[NSString stringWithFormat:@"eu1.%@", kCTApiDomain]]; - } - [self.urlSessionDelegate pinSSLCerts:sslCertNames forDomains:domains]; - self.urlSession = [NSURLSession sessionWithConfiguration:sc delegate:self.urlSessionDelegate delegateQueue:nil]; + self.urlSessionDelegate = [[CTPinnedNSURLSessionDelegate alloc] initWithConfig:self.config]; + self.domainFactory = [[CTDomainFactory alloc]initWithConfig:self.config pinnedNSURLSessionDelegate: self.urlSessionDelegate sslCertNames: sslCertNames]; + self.requestSender = [[CTRequestSender alloc]initWithConfig:self.config redirectDomain:self.domainFactory.redirectDomain pinnedNSURLSessionDelegate: self.urlSessionDelegate sslCertNames: sslCertNames]; #else - _sslPinningEnabled = NO; - self.urlSession = [NSURLSession sessionWithConfiguration:sc]; + self.domainFactory = [[CTDomainFactory alloc]initWithConfig:self.config]; + + self.requestSender = [[CTRequestSender alloc]initWithConfig:self.config redirectDomain:self.domainFactory.redirectDomain]; #endif - } + [self doHandshakeAsyncWithCompletion:nil]; } - (void)setUserSetLocation:(CLLocationCoordinate2D)location { @@ -860,116 +843,36 @@ - (CLLocationCoordinate2D)userSetLocation { # pragma mark - Handshake Handling -- (void)clearRedirectDomain { - self.redirectDomain = nil; - self.redirectNotifViewedDomain = nil; - [self persistRedirectDomain]; // if nil persist will remove - self.redirectDomain = [self loadRedirectDomain]; // reload explicit domain if we have one else will be nil - self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; // reload explicit notification viewe domain if we have one else will be nil -} - -- (NSString *)loadRedirectDomain { - NSString *region = self.config.accountRegion; - if (region) { - region = [region stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; - if (region.length > 0) { - self.explictEndpointDomain = [NSString stringWithFormat:@"%@.%@", region, kCTApiDomain]; - return self.explictEndpointDomain; - } - } - NSString *proxyDomain = self.config.proxyDomain; - if (proxyDomain) { - proxyDomain = [proxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; - if (proxyDomain.length > 0) { - self.explictEndpointDomain = proxyDomain; - return self.explictEndpointDomain; - } - } - NSString *domain = nil; - if (self.config.isDefaultInstance) { - domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_DOMAIN_KEY config: self.config] withResetValue:[CTPreferences getStringForKey:kREDIRECT_DOMAIN_KEY withResetValue:nil]]; - } else { - domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_DOMAIN_KEY config: self.config] withResetValue:nil]; - } - return domain; -} - -- (NSString *)loadRedirectNotifViewedDomain { - NSString *region = self.config.accountRegion; - if (region) { - region = [region stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; - if (region.length > 0) { - self.explictNotifViewedEndpointDomain = [NSString stringWithFormat:@"%@-%@", region, kCTNotifViewedApiDomain]; - return self.explictNotifViewedEndpointDomain; - } - } - NSString *spikyProxyDomain = self.config.spikyProxyDomain; - if (spikyProxyDomain) { - spikyProxyDomain = [spikyProxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].lowercaseString; - if (spikyProxyDomain.length > 0) { - self.explictNotifViewedEndpointDomain = spikyProxyDomain; - return self.explictNotifViewedEndpointDomain; - } - } - NSString *domain = nil; - if (self.config.isDefaultInstance) { - domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config] withResetValue:[CTPreferences getStringForKey:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY withResetValue:nil]]; - } else { - domain = [CTPreferences getStringForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config] withResetValue:nil]; - } - return domain; -} - -- (void)persistRedirectDomain { - if (self.redirectDomain != nil) { - [CTPreferences putString:self.redirectDomain forKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_DOMAIN_KEY config: self.config]]; -#if CLEVERTAP_SSL_PINNING - [self.urlSessionDelegate pinSSLCerts:sslCertNames forDomains:@[kCTApiDomain, self.redirectDomain]]; -#endif - } else { - [CTPreferences removeObjectForKey:kREDIRECT_DOMAIN_KEY]; - [CTPreferences removeObjectForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_DOMAIN_KEY config: self.config]]; - } -} - -- (void)persistRedirectNotifViewedDomain { - if (self.redirectNotifViewedDomain != nil) { - [CTPreferences putString:self.redirectNotifViewedDomain forKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config]]; -#if CLEVERTAP_SSL_PINNING - [self.urlSessionDelegate pinSSLCerts:sslCertNames forDomains:@[kCTNotifViewedApiDomain, self.redirectNotifViewedDomain]]; -#endif - } else { - [CTPreferences removeObjectForKey:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY]; - [CTPreferences removeObjectForKey:[CTPreferences storageKeyWithSuffix:kREDIRECT_NOTIF_VIEWED_DOMAIN_KEY config: self.config]]; - } -} - (void)persistMutedTs { self.lastMutedTs = [NSDate new].timeIntervalSince1970; [CTPreferences putInt:self.lastMutedTs forKey:[CTPreferences storageKeyWithSuffix:kMUTED_TS_KEY config: self.config]]; } - (BOOL)needHandshake { - if ([self isMuted] || self.explictEndpointDomain) { + if ([self isMuted] || self.domainFactory.explictEndpointDomain) { return NO; } - return self.redirectDomain == nil; + return self.domainFactory.redirectDomain == nil; } -- (void)doHandshakeAsync { +- (void)doHandshakeAsyncWithCompletion:(void (^ _Nullable )(void))taskBlock { [self runSerialAsync:^{ if (![self needHandshake]) { - //self.redirectDomain contains value + //self.domainFactory.redirectDomain contains value [self onDomainAvailable]; + if (taskBlock) { + taskBlock(); + } return; } CleverTapLogInternal(self.config.logLevel, @"%@: starting handshake with %@", self, kHANDSHAKE_URL); - NSMutableURLRequest *request = [self createURLRequestFromURL:[[NSURL alloc] initWithString:kHANDSHAKE_URL]]; - request.HTTPMethod = @"GET"; + // Need to simulate a synchronous request dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - NSURLSessionDataTask *task = [self.urlSession - dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + CTRequest *ctRequest = [CTRequestFactory helloRequestWithConfig:self.config]; + [ctRequest onResponse:^(NSData * _Nullable data, NSURLResponse * _Nullable response) { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode == 200) { @@ -982,9 +885,17 @@ - (void)doHandshakeAsync { } else { [self onDomainUnavailable]; } + if (taskBlock) { + taskBlock(); + } + + dispatch_semaphore_signal(semaphore); + }]; + [ctRequest onError:^(NSError * _Nullable error) { + [self onDomainUnavailable]; dispatch_semaphore_signal(semaphore); }]; - [task resume]; + [self.requestSender send:ctRequest]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }]; } @@ -995,12 +906,12 @@ - (BOOL)updateStateFromResponseHeadersShouldRedirectForNotif:(NSDictionary *)hea @try { NSString *redirectNotifViewedDomain = headers[kREDIRECT_NOTIF_VIEWED_HEADER]; if (redirectNotifViewedDomain != nil) { - NSString *currentDomain = self.redirectNotifViewedDomain; - self.redirectNotifViewedDomain = redirectNotifViewedDomain; - if (![self.redirectNotifViewedDomain isEqualToString:currentDomain]) { + NSString *currentDomain = self.domainFactory.redirectNotifViewedDomain; + self.domainFactory.redirectNotifViewedDomain = redirectNotifViewedDomain; + if (![self.domainFactory.redirectNotifViewedDomain isEqualToString:currentDomain]) { shouldRedirect = YES; - self.redirectNotifViewedDomain = redirectNotifViewedDomain; - [self persistRedirectNotifViewedDomain]; + self.domainFactory.redirectNotifViewedDomain = redirectNotifViewedDomain; + [self.domainFactory persistRedirectNotifViewedDomain]; } } NSString *mutedString = headers[kMUTE_HEADER]; @@ -1021,12 +932,12 @@ - (BOOL)updateStateFromResponseHeadersShouldRedirect:(NSDictionary *)headers { @try { NSString *redirectDomain = headers[kREDIRECT_HEADER]; if (redirectDomain != nil) { - NSString *currentDomain = self.redirectDomain; - self.redirectDomain = redirectDomain; - if (![self.redirectDomain isEqualToString:currentDomain]) { + NSString *currentDomain = self.domainFactory.redirectDomain; + self.domainFactory.redirectDomain = redirectDomain; + if (![self.domainFactory.redirectDomain isEqualToString:currentDomain]) { shouldRedirect = YES; - self.redirectDomain = redirectDomain; - [self persistRedirectDomain]; + self.domainFactory.redirectDomain = redirectDomain; + [self.domainFactory persistRedirectDomain]; //domain changed [self onDomainAvailable]; } @@ -1061,7 +972,7 @@ - (void)handleSendQueueSuccess { - (void)handleSendQueueFail { self.sendQueueFails += 1; if (self.sendQueueFails > 5) { - [self clearRedirectDomain]; + [self.domainFactory clearRedirectDomain]; self.sendQueueFails = 0; } } @@ -1069,28 +980,15 @@ - (void)handleSendQueueFail { #pragma mark - Queue/Dispatch helpers -- (NSMutableURLRequest *)createURLRequestFromURL:(NSURL *)url { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - NSString *accountId = self.config.accountId; - NSString *accountToken = self.config.accountToken; - if (accountId) { - [request setValue:accountId forHTTPHeaderField:kACCOUNT_ID_HEADER]; - } - if (accountToken) { - [request setValue:accountToken forHTTPHeaderField:kACCOUNT_TOKEN_HEADER]; - } - return request; -} - - (NSString *)endpointForQueue: (NSMutableArray *)queue { - if (!self.redirectDomain) return nil; + if (!self.domainFactory.redirectDomain) return nil; NSString *accountId = self.config.accountId; NSString *sdkRevision = self.deviceInfo.sdkVersion; NSString *endpointDomain; if (queue == _notificationsQueue) { - endpointDomain = self.redirectNotifViewedDomain; + endpointDomain = self.domainFactory.redirectNotifViewedDomain; } else { - endpointDomain = self.redirectDomain; + endpointDomain = self.domainFactory.redirectDomain; } NSString *endpointUrl = [[NSString alloc] initWithFormat:@"https://%@/a1?os=iOS&t=%@&z=%@", endpointDomain, sdkRevision, accountId]; currentRequestTimestamp = (int) [[[NSDate alloc] init] timeIntervalSince1970]; @@ -1177,8 +1075,6 @@ - (NSArray *)insertHeader:(NSDictionary *)header inBatch:(NSArray *)batch { - (NSDictionary *)generateAppFields { NSMutableDictionary *evtData = [NSMutableDictionary new]; - evtData[@"scv"] = self.deviceInfo.signedCallSDKVersion; - evtData[@"Version"] = self.deviceInfo.appVersion; evtData[@"Build"] = self.deviceInfo.appBuild; @@ -1234,6 +1130,12 @@ - (NSDictionary *)generateAppFields { evtData[@"lib"] = self.deviceInfo.library; } + if (auxiliarySdkVersions && auxiliarySdkVersions.count > 0) { + [auxiliarySdkVersions enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { + [evtData setObject:value forKey:key]; + }]; + } + #if CLEVERTAP_SSL_PINNING evtData[@"sslpin"] = @YES; #endif @@ -1253,26 +1155,6 @@ - (NSDictionary *)generateAppFields { return evtData; } -- (NSString *)jsonObjectToString:(id)object { - if ([object isKindOfClass:[NSString class]]) { - return object; - } - @try { - NSError *error; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:object - options:0 - error:&error]; - if (error) { - return @""; - } - NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - return jsonString; - } - @catch (NSException *exception) { - return @""; - } -} - - (id)convertDataToPrimitive:(id)event { @try { if ([event isKindOfClass:[NSArray class]]) { @@ -1386,7 +1268,7 @@ - (void)applicationDidEnterBackground:(NSNotification *)notification { - (void)applicationWillEnterForeground:(NSNotificationCenter *)notification { if ([self needHandshake]) { - [self doHandshakeAsync]; + [self doHandshakeAsyncWithCompletion:nil]; } } @@ -1438,20 +1320,32 @@ - (void)_appEnteredForeground { - (void)_appEnteredBackground { self.isAppForeground = NO; - if (![self isMuted]) { - [self persistQueues]; + + UIApplication *application = [[self class]getSharedApplication]; + UIBackgroundTaskIdentifier __block backgroundTask; + + void (^finishTaskHandler)(void) = ^(){ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + }); + }; + // Start background task to make sure it runs when the app is in background. + backgroundTask = [application beginBackgroundTaskWithExpirationHandler:finishTaskHandler]; + + @try { + [self persistOrClearQueues]; + [self updateSessionTime:(long) [[NSDate date] timeIntervalSince1970]]; + finishTaskHandler(); + } + @catch (NSException *exception) { + CleverTapLogDebug(self.config.logLevel, @"%@: Exception caught: %@", self, [exception reason]); + finishTaskHandler(); } - [self runSerialAsync:^{ - @try { - [self updateSessionTime:(long) [[NSDate date] timeIntervalSince1970]]; - } - @catch (NSException *exception) { - CleverTapLogDebug(self.config.logLevel, @"%@: Exception caught: %@", self, [exception reason]); - } - }]; } - (void)recordAppLaunched:(NSString *)caller { + if ([[self class] runningInsideAppExtension]) return; if (self.appLaunchProcessed) { @@ -1459,6 +1353,9 @@ - (void)recordAppLaunched:(NSString *)caller { return; } + // Load Vars from cache before App Launched + [self.variables.varCache loadDiffs]; + self.appLaunchProcessed = YES; if (self.config.disableAppLaunchedEvent) { @@ -2709,7 +2606,7 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp } } - CleverTapLogDebug(self.config.logLevel, @"%@: New event processed: %@", self, [self jsonObjectToString:mutableEvent]); + CleverTapLogDebug(self.config.logLevel, @"%@: New event processed: %@", self, [CTUtils jsonObjectToString:mutableEvent]); if (eventType == CleverTapEventTypeFetch) { [self flushQueue]; @@ -2732,7 +2629,7 @@ - (void)scheduleQueueFlush { - (void)flushQueue { if ([self needHandshake]) { [self runSerialAsync:^{ - [self doHandshakeAsync]; + [self doHandshakeAsyncWithCompletion:nil]; }]; } [self runSerialAsync:^{ @@ -2808,22 +2705,22 @@ - (void)clearNotificationsQueue { self.notificationsQueue = [NSMutableArray array]; } +- (void)persistOrClearQueues { + if ([self isMuted]) { + [self clearQueues]; + } else { + [self persistProfileQueue]; + [self persistEventsQueue]; + [self persistNotificationsQueue]; + } +} + - (void)persistQueues { [self runSerialAsync:^{ - @try { - if ([self isMuted]) { - [self clearQueues]; - } else { - [self persistProfileQueue]; - [self persistEventsQueue]; - [self persistNotificationsQueue]; - } - } - @catch (NSException *exception) { - CleverTapLogDebug(self.config.logLevel, @"%@: Exception caught: %@", self, [exception reason]); - } + [self persistOrClearQueues]; }]; } + - (void)persistEventsQueue { NSString *fileName = [self eventsFileName]; NSMutableArray *eventsCopy; @@ -2901,7 +2798,7 @@ - (void)sendQueue:(NSMutableArray *)queue { CleverTapLogInternal(self.config.logLevel, @"%@: Pending events batch contains: %d items", self, (int) [batch count]); @try { - NSString *jsonBody = [self jsonObjectToString:batchWithHeader]; + NSString *jsonBody = [CTUtils jsonObjectToString:batchWithHeader]; CleverTapLogDebug(self.config.logLevel, @"%@: Sending %@ to servers at %@", self, jsonBody, endpoint); @@ -2912,10 +2809,6 @@ - (void)sendQueue:(NSMutableArray *)queue { return; } - NSMutableURLRequest *request = [self createURLRequestFromURL:[[NSURL alloc] initWithString:endpoint]]; - request.HTTPBody = [jsonBody dataUsingEncoding:NSUTF8StringEncoding]; - request.HTTPMethod = @"POST"; - __block BOOL success = NO; __block NSData *responseData; @@ -2923,15 +2816,11 @@ - (void)sendQueue:(NSMutableArray *)queue { // Need to simulate a synchronous request dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - NSURLSessionDataTask *postDataTask = [self.urlSession - dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + CTRequest *ctRequest = [CTRequestFactory eventRequestWithConfig:self.config params:batchWithHeader url:endpoint]; + [ctRequest onResponse:^(NSData * _Nullable data, NSURLResponse * _Nullable response) { responseData = data; - if (error) { - CleverTapLogDebug(self.config.logLevel, @"%@: Network error while sending queue, will retry: %@", self, error.localizedDescription); - } - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; @@ -2951,7 +2840,14 @@ - (void)sendQueue:(NSMutableArray *)queue { dispatch_semaphore_signal(semaphore); }]; - [postDataTask resume]; + [ctRequest onError:^(NSError * _Nullable error) { + if (error) { + CleverTapLogDebug(self.config.logLevel, @"%@: Network error while sending queue, will retry: %@", self, error.localizedDescription); + } + [[self variables] handleVariablesError]; + dispatch_semaphore_signal(semaphore); + }]; + [self.requestSender send:ctRequest]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); if (!success) { @@ -3145,6 +3041,12 @@ - (void)parseResponse:(NSData *)responseData { } #endif + // Handle and Cache PE Variables + NSDictionary *varsResponse = jsonResp[CLTAP_PE_VARS_RESPONSE_KEY]; + if (varsResponse) { + [[self variables] handleVariablesResponse: jsonResp[CLTAP_PE_VARS_RESPONSE_KEY]]; + } + // Handle events/profiles sync data @try { NSDictionary *evpr = jsonResp[@"evpr"]; @@ -3389,6 +3291,8 @@ - (void) _asyncSwitchUser:(NSDictionary *)properties withCachedGuid:(NSString *) [self _resetProductConfig]; + [self _resetVars]; + // push data on reset profile [self recordAppLaunched:action]; if (properties) { @@ -4002,6 +3906,13 @@ - (void)setLibrary:(NSString *)name { self.deviceInfo.library = name; } +- (void)setCustomSdkVersion:(NSString *)name version:(int)version { + if (!auxiliarySdkVersions) { + auxiliarySdkVersions = [NSMutableDictionary new]; + } + auxiliarySdkVersions[name] = @(version); +} + + (void)setDebugLevel:(int)level { [CTLogger setDebugLevel:level]; if (_defaultInstanceConfig) { @@ -4040,14 +3951,14 @@ + (void)setCredentialsWithAccountID:(NSString *)accountID token:(NSString *)toke + (void)setCredentialsWithAccountID:(NSString *)accountID token:(NSString *)token proxyDomain:(NSString *)proxyDomain spikyProxyDomain:(NSString *)spikyProxyDomain { [self _setCredentialsWithAccountID:accountID token:token proxyDomain:proxyDomain]; - NSString *spikyProxyDomainResult; + NSString *finalSpikyProxyDomain; if (spikyProxyDomain != nil && ![spikyProxyDomain isEqualToString:@""]) { - spikyProxyDomainResult = [spikyProxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if (spikyProxyDomainResult.length <= 0) { - spikyProxyDomainResult = nil; + finalSpikyProxyDomain = [spikyProxyDomain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (finalSpikyProxyDomain.length <= 0) { + finalSpikyProxyDomain = nil; } } - [_plistInfo setCredentialsWithAccountID:accountID token:token proxyDomain:proxyDomain spikyProxyDomain:spikyProxyDomainResult]; + [_plistInfo setCredentialsWithAccountID:accountID token:token proxyDomain:proxyDomain spikyProxyDomain:finalSpikyProxyDomain]; } + (void)enablePersonalization { @@ -4104,7 +4015,9 @@ + (void)getLocationWithSuccess:(void (^)(CLLocationCoordinate2D location))succes 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); + if (error) { + error(errorMsg); + } } #endif } @@ -4853,6 +4766,13 @@ - (void)_resetProductConfig { } } +// run off main +- (void)_resetVars { + /// Clear content for current user + /// Content for new user will be loaded in `recordAppLaunched:` using `CTVarCache.loadDiffs` + [[self variables] clearUserContent]; +} + - (NSDictionary *)_setProductConfig:(NSDictionary *)arp { if (arp) { NSMutableDictionary *configOptions = [NSMutableDictionary new]; @@ -4985,10 +4905,6 @@ - (void)recordSignedCallEvent:(int)eventRawValue forCallDetails:(NSDictionary *) #endif } -- (void)setSignedCallVersion:(NSString *)version { - [self.deviceInfo setSignedCallSDKVersion: version]; -} - - (void)setDomainDelegate:(id)delegate { if ([[self class] runningInsideAppExtension]){ CleverTapLogDebug(self.config.logLevel, @"%@: setDomainDelegate is a no-op in an app extension.", self); @@ -5018,8 +4934,8 @@ - (void)onDomainUnavailable { //Updates the format of the domain - from `in1.clevertap-prod.com` to region.auth.domain (i.e. in1.auth.clevertap-prod.com) - (NSString *)getDomainString { - if (self.redirectDomain != nil) { - NSArray *listItems = [self.redirectDomain componentsSeparatedByString:@"."]; + if (self.domainFactory.redirectDomain != nil) { + NSArray *listItems = [self.domainFactory.redirectDomain componentsSeparatedByString:@"."]; NSString *domainItem = [listItems[0] stringByAppendingString:@".auth"]; for (int i = 1; i < listItems.count; i++ ) { NSString *dotString = [@"." stringByAppendingString: listItems[i]]; @@ -5177,4 +5093,214 @@ + (BOOL)isValidCleverTapId:(NSString *_Nullable)cleverTapID { return [CTValidator isValidCleverTapId:cleverTapID]; } +#pragma mark - Product Experiences + +- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onVariablesChanged:block]; +} + +- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onceVariablesChanged:block]; +} + +- (void)syncVariables { + [self syncVariables:NO]; +} + +- (void)syncVariablesEnsureHandshake { + if ([self needHandshake]) { + [self runSerialAsync:^{ + [self doHandshakeAsyncWithCompletion:^{ + [self _syncVars]; + }]; + }]; + } + else { + [self runSerialAsync:^{ + [self _syncVars]; + }]; + } +} + +- (void)syncVariables:(BOOL)isProduction { + if (isProduction) { +#if DEBUG + CleverTapLogInfo(_config.logLevel, @"%@: Calling syncVariables: with isProduction:YES from Debug configuration/build. Use syncVariables in this case", self); +#else + CleverTapLogInfo(_config.logLevel, @"%@: Calling syncVariables: with isProduction:YES from Release configuration/build. Do not release this build and use with caution", self); +#endif + [self syncVariablesEnsureHandshake]; + } else { +#if DEBUG + [self syncVariablesEnsureHandshake]; +#else + CleverTapLogInfo(_config.logLevel, @"%@: syncVariables can only be called from Debug configurations/builds", self); +#endif + } +} + +- (void)_syncVars { + NSDictionary *meta = [self batchHeader]; + NSDictionary *varsPayload = [[self variables] varsPayload]; + NSArray *payload = @[meta,varsPayload]; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + NSString *url = [NSString stringWithFormat:@"https://%@/%@",self.domainFactory.redirectDomain, CT_PE_DEFINE_VARS_ENDPOINT]; + CTRequest *ctRequest = [CTRequestFactory syncVarsRequestWithConfig:self.config params:payload url:url]; + + [ctRequest onResponse:^(NSData * _Nullable data, NSURLResponse * _Nullable response) { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + CleverTapLogDebug(self->_config.logLevel, @"%@: Vars synced successfully", self); + } + else if (httpResponse.statusCode == 401) { + CleverTapLogDebug(self->_config.logLevel, @"%@: Unauthorized access from a non-test profile. Please mark this profile as a test profile from the CleverTap dashboard.", self); + } + } + CT_TRY + id jsonResp = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if (jsonResp[@"error"]) { + CleverTapLogDebug(self->_config.logLevel, @"%@: Error while syncing vars: %@", self, jsonResp[@"error"]); + } + CT_END_TRY + dispatch_semaphore_signal(semaphore); + }]; + [ctRequest onError:^(NSError * _Nullable error) { + CleverTapLogDebug(self->_config.logLevel, @"%@: error syncing vars: %@", self, error.debugDescription); + dispatch_semaphore_signal(semaphore); + }]; + [self.requestSender send:ctRequest]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); +} + +- (void)fetchVariables:(CleverTapFetchVariablesBlock)block { + [[self variables] setFetchVariablesBlock:block]; + [self queueEvent:@{@"evtName": CLTAP_WZRK_FETCH_EVENT, @"evtData" : @{@"t": @4}} withType:CleverTapEventTypeFetch]; +} + +- (CTVar * _Nullable)getVariable:(NSString * _Nonnull)name { + CTVar *var = [[self.variables varCache] getVariable:name]; + if (!var) { + CleverTapLogDebug(self.config.logLevel, @"%@: Variable with name: %@ not found.", self, name); + } + return var; +} + +- (id _Nullable)getVariableValue:(NSString * _Nonnull)name { + return [[self.variables varCache] getMergedValue:name]; +} + +#pragma mark - PE Vars + +- (CTVar *)defineVar:(NSString *)name { + return [self.variables define:name with:nil kind:nil]; +} + +- (CTVar *)defineVar:(NSString *)name withInt:(int)defaultValue { + return [self.variables define:name with:[NSNumber numberWithInt:defaultValue] kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withFloat:(float)defaultValue { + return [self.variables define:name with:[NSNumber numberWithFloat:defaultValue] kind:CT_KIND_FLOAT]; +} + +- (CTVar *)defineVar:(NSString *)name withDouble:(double)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithDouble:defaultValue] + kind:CT_KIND_FLOAT]; +} + +- (CTVar *)defineVar:(NSString *)name withCGFloat:(CGFloat)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithDouble:defaultValue] + kind:CT_KIND_FLOAT]; +} + +- (CTVar *)defineVar:(NSString *)name withShort:(short)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithShort:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withChar:(char)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithChar:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withBool:(BOOL)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithBool:defaultValue] + kind:CT_KIND_BOOLEAN]; +} + +- (CTVar *)defineVar:(NSString *)name withInteger:(NSInteger)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithInteger:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withLong:(long)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithLong:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withLongLong:(long long)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithLongLong:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedChar:(unsigned char)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithUnsignedChar:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedInt:(unsigned int)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithUnsignedInt:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedInteger:(NSUInteger)defaultValue +{ + return [self.variables define:name + with:[NSNumber numberWithUnsignedInteger:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedLong:(unsigned long)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithUnsignedLong:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedLongLong:(unsigned long long)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithUnsignedLongLong:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withUnsignedShort:(unsigned short)defaultValue { + return [self.variables define:name + with:[NSNumber numberWithUnsignedShort:defaultValue] + kind:CT_KIND_INT]; +} + +- (CTVar *)defineVar:(NSString *)name withString:(NSString *)defaultValue { + return [self.variables define:name with:defaultValue kind:CT_KIND_STRING]; +} + +- (CTVar *)defineVar:(NSString *)name withNumber:(NSNumber *)defaultValue { + return [self.variables define:name with:defaultValue kind:CT_KIND_FLOAT]; +} + +- (CTVar *)defineVar:(NSString *)name withDictionary:(NSDictionary *)defaultValue { + return [self.variables define:name with:defaultValue kind:CT_KIND_DICTIONARY]; +} + @end diff --git a/CleverTapSDK/CleverTapBuildInfo.h b/CleverTapSDK/CleverTapBuildInfo.h index a3409210..64a64c5a 100644 --- a/CleverTapSDK/CleverTapBuildInfo.h +++ b/CleverTapSDK/CleverTapBuildInfo.h @@ -1,3 +1 @@ - -#define WR_SDK_REVISION @"40202" - +#define WR_SDK_REVISION @"50000" diff --git a/CleverTapSDK/CleverTapJSInterface.m b/CleverTapSDK/CleverTapJSInterface.m index 5d32f950..2c9231d5 100644 --- a/CleverTapSDK/CleverTapJSInterface.m +++ b/CleverTapSDK/CleverTapJSInterface.m @@ -41,7 +41,7 @@ - (void)userContentController:(nonnull WKUserContentController *)userContentCont - (void)handleMessageFromWebview:(NSDictionary *)message forInstance:(CleverTap *)cleverTap { NSString *action = [message objectForKey:@"action"]; if ([action isEqual:@"recordEventWithProps"]) { - [cleverTap recordEvent: message[@"event"] withProps: message[@"props"]]; + [cleverTap recordEvent: message[@"event"] withProps: message[@"properties"]]; } else if ([action isEqual: @"profilePush"]) { [cleverTap profilePush: message[@"properties"]]; } else if ([action isEqual: @"profileSetMultiValues"]) { diff --git a/CleverTapSDK/ProductConfig/controllers/CTProductConfigController.m b/CleverTapSDK/ProductConfig/controllers/CTProductConfigController.m index bfde4a3c..e1870d66 100644 --- a/CleverTapSDK/ProductConfig/controllers/CTProductConfigController.m +++ b/CleverTapSDK/ProductConfig/controllers/CTProductConfigController.m @@ -1,7 +1,6 @@ #import "CTProductConfigController.h" #import "CTConstants.h" #import "CTPreferences.h" -#import "CTPreferences.h" #import "CleverTapInstanceConfig.h" #import "CleverTapProductConfigPrivate.h" diff --git a/CleverTapSDK/ProductExperiences/CTVar-Internal.h b/CleverTapSDK/ProductExperiences/CTVar-Internal.h new file mode 100644 index 00000000..10954bc5 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVar-Internal.h @@ -0,0 +1,31 @@ +#import "CTVar.h" +@class CTVarCache; + +NS_ASSUME_NONNULL_BEGIN + +@interface CTVar () + +- (instancetype)initWithName:(NSString *)name + withDefaultValue:(NSObject *)defaultValue + withKind:(NSString *)kind + varCache:(CTVarCache *)cache; + +@property (readonly, strong) CTVarCache *varCache; +@property (readonly, strong) NSString *name; +@property (readonly, strong) NSArray *nameComponents; +@property (readonly) BOOL hadStarted; +@property (readonly, strong) NSString *kind; +@property (readonly, strong) NSMutableArray *valueChangedBlocks; +@property (nonatomic, unsafe_unretained, nullable) id delegate; +@property (readonly) BOOL hasChanged; + +- (void)update; +- (void)cacheComputedValues; +- (void)triggerValueChanged; + ++ (BOOL)printedCallbackWarning; ++ (void)setPrintedCallbackWarning:(BOOL)newPrintedCallbackWarning; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/ProductExperiences/CTVar.h b/CleverTapSDK/ProductExperiences/CTVar.h new file mode 100644 index 00000000..8c347875 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVar.h @@ -0,0 +1,108 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^CleverTapVariablesChangedBlock)(void); +typedef void (^CleverTapFetchVariablesBlock)(BOOL success); + +@class CTVar; +/** + * Receives callbacks for {@link CTVar} + */ +NS_SWIFT_NAME(VarDelegate) +@protocol CTVarDelegate +@optional +/** + * Called when the value of the variable changes. + */ +- (void)valueDidChange:(CTVar *)variable; +@end + +/** + * A variable is any part of your application that can change from an experiment. + * Check out {@link Macros the macros} for defining variables more easily. + */ +NS_SWIFT_NAME(Var) +@interface CTVar : NSObject + +@property (readonly, strong, nullable) NSString *stringValue; +@property (readonly, strong, nullable) NSNumber *numberValue; +@property (readonly, strong, nullable) id value; +@property (readonly, strong, nullable) id defaultValue; + +/** + * @{ + * Defines a {@link LPVar} + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Returns the name of the variable. + */ +- (NSString *)name; + +/** + * Returns the components of the variable's name. + */ +- (NSArray *)nameComponents; + +/** + * Returns the default value of a variable. + */ +- (nullable id)defaultValue; + +/** + * Returns the kind of the variable. + */ +- (NSString *)kind; + +/** + * Returns whether the variable has changed since the last time the app was run. + */ +- (BOOL)hasChanged; + +/** + * Called when the value of the variable changes. + */ +- (void)onValueChanged:(CleverTapVariablesChangedBlock)block; + +/** + * Sets the delegate of the variable in order to use + * {@link CTVarDelegate::valueDidChange:} + */ +- (void)setDelegate:(nullable id )delegate; + +- (void)clearState; + +/** + * @{ + * Accessess the value(s) of the variable + */ +- (id)objectForKey:(nullable NSString *)key; +- (id)objectAtIndex:(NSUInteger )index; +- (id)objectForKeyPath:(nullable id)firstComponent, ... NS_REQUIRES_NIL_TERMINATION; +- (id)objectForKeyPathComponents:(nullable NSArray *)pathComponents; + +- (nullable NSNumber *)numberValue; +- (nullable NSString *)stringValue; +- (int)intValue; +- (double)doubleValue; +- (CGFloat)cgFloatValue; +- (float)floatValue; +- (short)shortValue; +- (BOOL)boolValue; +- (char)charValue; +- (long)longValue; +- (long long)longLongValue; +- (NSInteger)integerValue; +- (unsigned char)unsignedCharValue; +- (unsigned short)unsignedShortValue; +- (unsigned int)unsignedIntValue; +- (NSUInteger)unsignedIntegerValue; +- (unsigned long)unsignedLongValue; +- (unsigned long long)unsignedLongLongValue; +/**@}*/ +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/ProductExperiences/CTVar.m b/CleverTapSDK/ProductExperiences/CTVar.m new file mode 100644 index 00000000..53499d53 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVar.m @@ -0,0 +1,224 @@ +#import "CTVar-Internal.h" +#import "CTVarCache.h" +#import "CTConstants.h" + +static BOOL LPVAR_PRINTED_CALLBACK_WARNING = NO; + +@interface CTVar (PrivateProperties) +@property (nonatomic, strong) CTVarCache *varCache; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSArray *nameComponents; +@property (nonatomic, strong) NSString *stringValue; +@property (nonatomic, strong) NSNumber *numberValue; +@property (nonatomic) BOOL hadStarted; +@property (nonatomic, strong) id value; +@property (nonatomic, strong) id defaultValue; +@property (nonatomic, strong) NSString *kind; +@property (nonatomic, strong) NSMutableArray *valueChangedBlocks; +@property (nonatomic) BOOL hasChanged; +@end + +@implementation CTVar + +- (instancetype)initWithName:(NSString *)name withDefaultValue:(NSNumber *)defaultValue + withKind:(NSString *)kind varCache:(CTVarCache *)cache +{ + self = [super init]; + if (self) { + CT_TRY + _name = name; + self.varCache = cache; + _nameComponents = [self.varCache getNameComponents:name]; + _defaultValue = defaultValue; + _value = defaultValue; + _kind = kind; + [self cacheComputedValues]; + + [self.varCache registerVariable:self]; + + [self update]; + CT_END_TRY + } + return self; +} + +// Manually @synthesize since CTVar provides custom getters/setters +// Properties are defined as readonly in CTVar-Internal +// and readwrite in PrivateProperties category +@synthesize stringValue = _stringValue; +@synthesize numberValue = _numberValue; +@synthesize varCache = _varCache; + +- (CTVarCache *)varCache { + return _varCache; +} + +- (void)setVarCache:(CTVarCache *)varCache { + _varCache = varCache; +} + +#pragma mark Updates + +- (void) cacheComputedValues { + // Cache computed values. + if ([_value isKindOfClass:NSString.class]) { + _stringValue = (NSString *) _value; + _numberValue = [NSNumber numberWithDouble:[_stringValue doubleValue]]; + } else if ([_value isKindOfClass:NSNumber.class]) { + _stringValue = [NSString stringWithFormat:@"%@", _value]; + _numberValue = (NSNumber *) _value; + } else { + _stringValue = nil; + _numberValue = nil; + } +} + +- (void)update { + NSObject *oldValue = _value; + _value = [self.varCache getMergedValueFromComponentArray:_nameComponents]; + + if ([_value isEqual:oldValue] && _hadStarted) { + return; + } + [self cacheComputedValues]; + + if (![_value isEqual:oldValue]) { + _hasChanged = YES; + } + + if ([[self varCache] hasVarsRequestCompleted]) { + [self triggerValueChanged]; + _hadStarted = YES; + } +} + +#pragma mark Callbacks + +- (void)triggerValueChanged { + if (self.delegate && + [self.delegate respondsToSelector:@selector(valueDidChange:)]) { + [self.delegate valueDidChange:self]; + } + + for (CleverTapVariablesChangedBlock block in _valueChangedBlocks.copy) { + block(); + } +} + +- (void)onValueChanged:(CleverTapVariablesChangedBlock)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CTVar onValueChanged]."); + return; + } + + CT_TRY + if (!_valueChangedBlocks) { + _valueChangedBlocks = [NSMutableArray array]; + } + [_valueChangedBlocks addObject:[block copy]]; + if ([[self varCache] hasVarsRequestCompleted]) { + [self triggerValueChanged]; + } + CT_END_TRY +} + +- (void)setDelegate:(id)delegate { + CT_TRY + _delegate = delegate; + if ([[self varCache] hasVarsRequestCompleted]) { + [self triggerValueChanged]; + } + CT_END_TRY +} + +#pragma mark Dictionary handling + +- (id) objectForKey:(NSString *)key { + return [self objectForKeyPath:key, nil]; +} + +- (id) objectAtIndex:(NSUInteger)index { + return [self objectForKeyPath:@(index), nil]; +} + +- (id) objectForKeyPath:(id)firstComponent, ... { + CT_TRY + [self warnIfNotStarted]; + NSMutableArray *components = [_nameComponents mutableCopy]; + va_list args; + va_start(args, firstComponent); + for (id component = firstComponent; + component != nil; component = va_arg(args, id)) { + [components addObject:component]; + } + va_end(args); + return [self.varCache getMergedValueFromComponentArray:components]; + CT_END_TRY + return nil; +} + +- (id)objectForKeyPathComponents:(NSArray *)pathComponents { + CT_TRY + [self warnIfNotStarted]; + NSMutableArray *components = [_nameComponents mutableCopy]; + [components addObjectsFromArray:pathComponents]; + return [self.varCache getMergedValueFromComponentArray:components]; + CT_END_TRY + return nil; +} + +#pragma mark Value accessors + +- (NSNumber *)numberValue { + [self warnIfNotStarted]; + return _numberValue; +} + +- (NSString *)stringValue { + [self warnIfNotStarted]; + return _stringValue; +} + +- (int)intValue { return [[self numberValue] intValue]; } +- (double)doubleValue { return [[self numberValue] doubleValue];} +- (float)floatValue { return [[self numberValue] floatValue]; } +- (CGFloat)cgFloatValue { return [[self numberValue] doubleValue]; } +- (short)shortValue { return [[self numberValue] shortValue];} +- (BOOL)boolValue { return [[self numberValue] boolValue]; } +- (char)charValue { return [[self numberValue] charValue]; } +- (long)longValue { return [[self numberValue] longValue]; } +- (long long)longLongValue { return [[self numberValue] longLongValue]; } +- (NSInteger)integerValue { return [[self numberValue] integerValue]; } +- (unsigned char)unsignedCharValue { return [[self numberValue] unsignedCharValue]; } +- (unsigned short)unsignedShortValue { return [[self numberValue] unsignedShortValue]; } +- (unsigned int)unsignedIntValue { return [[self numberValue] unsignedIntValue]; } +- (NSUInteger)unsignedIntegerValue { return [[self numberValue] unsignedIntegerValue]; } +- (unsigned long)unsignedLongValue { return [[self numberValue] unsignedLongValue]; } +- (unsigned long long)unsignedLongLongValue { return [[self numberValue] unsignedLongLongValue]; } + +#pragma mark Utils + ++ (BOOL)printedCallbackWarning { + return LPVAR_PRINTED_CALLBACK_WARNING; +} + ++ (void)setPrintedCallbackWarning:(BOOL)newPrintedCallbackWarning { + LPVAR_PRINTED_CALLBACK_WARNING = newPrintedCallbackWarning; +} + +- (void)warnIfNotStarted { + if (!self.varCache.hasVarsRequestCompleted && ![CTVar printedCallbackWarning]) { + CleverTapLogDebug(self.varCache.config.logLevel, @"%@: CleverTap hasn't finished retrieving values from the server. You " + @"should use a callback to make sure the value for '%@' is ready. Otherwise, your " + @"app may not use the most up-to-date value.", self, self.name); + + [CTVar setPrintedCallbackWarning:YES]; + } +} + +- (void)clearState { + _hadStarted = NO; + _hasChanged = NO; +} + +@end diff --git a/CleverTapSDK/ProductExperiences/CTVarCache.h b/CleverTapSDK/ProductExperiences/CTVarCache.h new file mode 100644 index 00000000..9a8a7996 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVarCache.h @@ -0,0 +1,35 @@ +#import +#import "CTVar-Internal.h" +#import "CleverTapInstanceConfig.h" +#import "CTDeviceInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^CacheUpdateBlock)(void); + +NS_SWIFT_NAME(VarCache) +@interface CTVarCache : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo; + +@property (nonatomic, strong, readonly) CleverTapInstanceConfig *config; +@property (strong, nonatomic) NSMutableDictionary *vars; +@property (assign, nonatomic) BOOL hasVarsRequestCompleted; + +- (nullable NSDictionary *)diffs; +- (void)loadDiffs; +- (void)applyVariableDiffs:(nullable NSDictionary *)diffs_; + +- (void)registerVariable:(CTVar *)var; +- (nullable CTVar *)getVariable:(NSString *)name; +- (id)getMergedValue:(NSString *)name; + +- (NSArray *)getNameComponents:(NSString *)name; +- (nullable id)getMergedValueFromComponentArray:(NSArray *) components; +- (void)clearUserContent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/ProductExperiences/CTVarCache.m b/CleverTapSDK/ProductExperiences/CTVarCache.m new file mode 100644 index 00000000..959ccc9f --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVarCache.m @@ -0,0 +1,264 @@ +#import "CTVarCache.h" +#import "CTUtils.h" +#import "CTConstants.h" +#import "CTPreferences.h" +#import "ContentMerger.h" + +@interface CTVarCache() +@property (strong, nonatomic) NSMutableDictionary *valuesFromClient; +@property (strong, nonatomic) id merged; +@property (strong, nonatomic) NSDictionary *diffs; + +@property (strong, nonatomic) CacheUpdateBlock updateBlock; +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTDeviceInfo *deviceInfo; +@end + +@implementation CTVarCache + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo { + if ((self = [super init])) { + self.config = config; + self.deviceInfo = deviceInfo; + [self initialize]; + } + return self; +} + +- (void)initialize { + self.vars = [NSMutableDictionary dictionary]; + self.diffs = [NSMutableDictionary dictionary]; + self.valuesFromClient = [NSMutableDictionary dictionary]; + self.hasVarsRequestCompleted = NO; +} + +- (NSArray *)getNameComponents:(NSString *)name { + NSArray *nameComponents = [name componentsSeparatedByString:@"."]; + return nameComponents; +} + +- (id)traverse:(id)collection withKey:(id)key autoInsert:(BOOL)autoInsert { + id result = nil; + if ([collection respondsToSelector:@selector(objectForKey:)]) { + result = [collection objectForKey:key]; + if (autoInsert && !result && [key isKindOfClass:NSString.class]) { + result = [NSMutableDictionary dictionary]; + [collection setObject:result forKey:key]; + } + } + + if ([result isKindOfClass:[NSNull class]]) { + return nil; + } + + return result; +} + +// Updates a JSON structure of variable values +- (void)updateValues:(NSString *)name + nameComponents:(NSArray *)nameComponents + value:(id)value + values:(NSMutableDictionary *)values +{ + if (value) { + id valuesPtr = values; + for (int i = 0; i < nameComponents.count - 1; i++) { + valuesPtr = [self traverse:valuesPtr withKey:nameComponents[i] autoInsert:YES]; + } + + // Make the value mutable. That way, if we add a dictionary variable, + // we can still add subvariables. + if ([value isKindOfClass:NSDictionary.class] && + [value class] != [NSMutableDictionary class]) { + value = [NSMutableDictionary dictionaryWithDictionary:value]; + } + + // Do not override variable dictionary values. If value is dictionary and + // already registered variable value is a dictionary, merge them. + // If values are not dictionaries, check if value from another variable will be overridden and log it. + id currentValue = valuesPtr[nameComponents.lastObject]; + if (currentValue && [currentValue isKindOfClass:NSDictionary.class] && [value isKindOfClass:NSMutableDictionary.class]) { + // Merge all entries from both dictionaries. NSMutableDictionary addEntriesFromDictionary: will not work for nested dictionaries. + value = [ContentMerger mergeWithVars:value diff:currentValue]; + } else if (currentValue && ![currentValue isEqual:value]) { + CleverTapLogInfo(self.config.logLevel, @"%@: Variable with name: %@ will override value: %@, with new value: %@.", self, name, currentValue, value); + } + + [valuesPtr setObject:value forKey:nameComponents.lastObject]; + } +} + +// Merge default variable value with VarCache.merged value +// This is neccessary if variable was registered after VarCache.applyVariableDiffs +- (void)mergeVariable:(CTVar * _Nonnull)var { + if (!self.merged || ![self.merged isKindOfClass:[NSMutableDictionary class]]) { + return; + } + + NSString *firstComponent = var.nameComponents.firstObject; + id defaultValue = [self.valuesFromClient objectForKey:firstComponent]; + id mergedValue = [self.merged objectForKey:firstComponent]; + + BOOL shouldMerge = (!defaultValue && mergedValue) || + (defaultValue && ![defaultValue isEqual:mergedValue]); + if (shouldMerge) { + id newValue = [ContentMerger mergeWithVars:defaultValue diff:mergedValue]; + if (newValue == nil) { + return; + } + [self.merged setObject:newValue forKey:firstComponent]; + + NSMutableString *name = [[NSMutableString alloc] initWithString:firstComponent]; + for (int i = 1; i < var.nameComponents.count; i++) + { + CTVar *existingVar = self.vars[name]; + if (existingVar) { + [existingVar update]; + break; + } + [name appendFormat:@".%@", var.nameComponents[i]]; + } + } +} + +- (void)registerVariable:(CTVar *)var { + [self.vars setObject:var forKey:var.name]; + + [self updateValues:var.name + nameComponents:var.nameComponents + value:var.defaultValue + values:self.valuesFromClient]; + + [self mergeVariable:var]; +} + +- (CTVar *)getVariable:(NSString *)name { + return [self.vars objectForKey:name]; +} + +- (id)getMergedValue:(NSString *)name { + NSArray *components = [self getNameComponents:name]; + id value = [self getMergedValueFromComponentArray:components]; + if ([value conformsToProtocol:@protocol(NSCopying)] && [value respondsToSelector:@selector(copyWithZone:)]) { + if ([value respondsToSelector:@selector(mutableCopyWithZone:)]) { + return [value mutableCopy]; + } + return [value copy]; + } + + return value; +} + +- (id)getValueFromComponentArray:(NSArray *) components fromDict:(NSDictionary *)values { + id mergedPtr = values; + for (id component in components) { + mergedPtr = [self traverse:mergedPtr withKey:component autoInsert:NO]; + } + return mergedPtr; +} + +- (id)getMergedValueFromComponentArray:(NSArray *)components { + return [self getValueFromComponentArray:components fromDict:self.merged ? self.merged : self.valuesFromClient]; +} + +- (void)loadDiffs { + @try { + NSString *fileName = [self dataArchiveFileName]; + NSString *filePath = [CTPreferences filePathfromFileName:fileName]; + NSData *diffsData = [NSData dataWithContentsOfFile:filePath]; + if (!diffsData) { + [self applyVariableDiffs:@{}]; + return; + } + NSKeyedUnarchiver *unarchiver; + if (@available(iOS 12.0, *)) { + NSError *error = nil; + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:diffsData error:&error]; + if (error != nil) { + CleverTapLogDebug(self.config.logLevel, @"%@: Error while loading variables: %@", self, error.localizedDescription); + return; + } + unarchiver.requiresSecureCoding = NO; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:diffsData]; +#pragma clang diagnostic pop + } + NSDictionary *diffs = (NSDictionary *) [unarchiver decodeObjectForKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + + [self applyVariableDiffs:diffs]; + } @catch (NSException *exception) { + CleverTapLogDebug(self.config.logLevel, @"%@: Error while loading variables: %@", self, exception.debugDescription); + } +} + +- (void)saveDiffs { + // Stores the variables on the device in case we don't have a connection. + // Restores next time when the app is opened. + // Diffs need to be locked incase other thread changes the diffs + @synchronized (self.diffs) { + NSMutableData *diffsData = [[NSMutableData alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:diffsData]; +#pragma clang diagnostic pop + [archiver encodeObject:self.diffs forKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + [archiver finishEncoding]; + + NSError *writeError = nil; + NSString *fileName = [self dataArchiveFileName]; + NSString *filePath = [CTPreferences filePathfromFileName:fileName]; + [diffsData writeToFile:filePath options:NSDataWritingAtomic error:&writeError]; + if (writeError) { + CleverTapLogStaticInternal(@"%@ failed to write data at %@: %@", self, filePath, writeError); + } + } +} + +- (NSString*)dataArchiveFileName { + return [NSString stringWithFormat:@"clevertap-%@-%@-pe-vars.plist", _config.accountId, _deviceInfo.deviceId]; +} + +- (void)applyVariableDiffs:(NSDictionary *)diffs_ { + CleverTapLogDebug(self.config.logLevel, @"%@: Applying Variables: %@", self, diffs_); + @synchronized (self.vars) { + // Prevent overriding variables if API returns null + // If no variables are defined, API returns {} + if (diffs_ != nil && ![diffs_ isEqual:[NSNull null]]) { + self.diffs = diffs_; + + // We need to lock it in case multiple threads will be accessing this. + @synchronized (self.diffs) { + self.merged = [ContentMerger mergeWithVars:self.valuesFromClient diff:self.diffs]; + } + + // Update variables with new values. + // Have to extract the keys because a dictionary variable may add a new sub-variable, + // modifying the variable dictionary. + for (NSString *name in [self.vars allKeys]) { + [self.vars[name] update]; + } + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: No variables received from the server", self); + } + + // Do NOT save diffs when loading from cache + // Load diffs is called before vars request has been sent + if (self.hasVarsRequestCompleted) { + [self saveDiffs]; + } + } +} + +- (void)clearUserContent { + // Disable callbacks and wait until fetch is finished + [self setHasVarsRequestCompleted:NO]; + // Clear Var state to allow callback invocation when server values are downloaded + [self.vars enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + CTVar *var = (CTVar *)obj; + [var clearState]; + }]; +} + +@end diff --git a/CleverTapSDK/ProductExperiences/CTVariables.h b/CleverTapSDK/ProductExperiences/CTVariables.h new file mode 100644 index 00000000..251d23fa --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVariables.h @@ -0,0 +1,40 @@ +// +// CTVariables.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 12.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTVarCache.h" +#import "CleverTapInstanceConfig.h" +#import "CTDeviceInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTVariables : NSObject + +@property(strong, nonatomic) CTVarCache *varCache; +@property(strong, nonatomic, nullable) CleverTapFetchVariablesBlock fetchVariablesBlock; + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo *)deviceInfo; + +- (CTVar *)define:(NSString *)name + with:(nullable NSObject *)defaultValue + kind:(nullable NSString *)kind +NS_SWIFT_NAME(define(name:value:kind:)); + +- (void)handleVariablesResponse:(NSDictionary *)varsResponse; +- (void)handleVariablesError; +- (void)triggerFetchVariables:(BOOL)success; +- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block; +- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block; +- (NSDictionary*)flatten:(NSDictionary*)map varName:(NSString*)varName; +- (NSDictionary*)varsPayload; +- (NSDictionary*)unflatten:(NSDictionary*)result; +- (void)clearUserContent; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/ProductExperiences/CTVariables.m b/CleverTapSDK/ProductExperiences/CTVariables.m new file mode 100644 index 00000000..fc607283 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/CTVariables.m @@ -0,0 +1,255 @@ +// +// CTVariables.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 12.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTVariables.h" +#import "CTConstants.h" +#import "CTUtils.h" + +@interface CTVariables() +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTDeviceInfo *deviceInfo; + +@property(strong, nonatomic) NSMutableArray *variablesChangedBlocks; +@property(strong, nonatomic) NSMutableArray *onceVariablesChangedBlocks; + +@end + +@implementation CTVariables + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo { + if ((self = [super init])) { + self.varCache = [[CTVarCache alloc] initWithConfig:config deviceInfo:deviceInfo]; + } + return self; +} + +#pragma mark Define Var +- (CTVar *)define:(NSString *)name with:(NSObject *)defaultValue kind:(NSString *)kind { + if ([CTUtils isNullOrEmpty:name]) { + CleverTapLogDebug(_config.logLevel, @"%@: Empty name provided as parameter while defining a variable.", self); + return nil; + } + + if ([name hasPrefix:@"."] || [name hasSuffix:@"."]) { + CleverTapLogDebug(_config.logLevel, @"%@: Variable name starts or ends with a `.` which is not allowed.", self); + return nil; + } + + @synchronized (self.varCache.vars) { + CT_TRY + CTVar *existing = [self.varCache getVariable:name]; + if (existing) { + return existing; + } + CT_END_TRY + CTVar *var = [[CTVar alloc] initWithName:name + withDefaultValue:defaultValue + withKind:kind + varCache:self.varCache]; + return var; + } +} + +#pragma mark Handle Response +- (void)handleVariablesResponse:(NSDictionary *)varsResponse { + if (varsResponse) { + CleverTapLogDebug(self.config.logLevel, @"%@: Handle Variables Response with: %@", self, varsResponse); + [[self varCache] setHasVarsRequestCompleted:YES]; + NSDictionary *values = [self unflatten:varsResponse]; + [[self varCache] applyVariableDiffs:values]; + [self triggerVariablesChanged]; + [self triggerFetchVariables:YES]; + } +} + +- (void)handleVariablesError { + CleverTapLogDebug(self.config.logLevel, @"%@: Handle Variables Error", self); + if (![[self varCache] hasVarsRequestCompleted]) { + [[self varCache] setHasVarsRequestCompleted:YES]; + // Ensure variables are loaded from cache. Triggers individual Vars update. + [[self varCache] loadDiffs]; + [self triggerVariablesChanged]; + } + + if (self.fetchVariablesBlock) { + [self triggerFetchVariables:NO]; + } +} + +- (void)clearUserContent { + [self.varCache clearUserContent]; +} + +#pragma mark Triggers +- (void)triggerFetchVariables:(BOOL)success { + if (self.fetchVariablesBlock) { + CleverTapFetchVariablesBlock block = [self.fetchVariablesBlock copy]; + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + block(success); + }); + } else { + block(success); + } + // The callback will be overridden by subsequent fetch call, + // if the first one has not completed yet. + // Callback cannot be attached to an individual fetch request, only to the queue batch. + self.fetchVariablesBlock = nil; + } +} + +- (void)triggerVariablesChanged { + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self triggerVariablesChanged]; + }); + return; + } + + for (CleverTapVariablesChangedBlock block in self.variablesChangedBlocks.copy) { + block(); + } + + NSArray *onceBlocksCopy; + @synchronized (self.onceVariablesChangedBlocks) { + onceBlocksCopy = self.onceVariablesChangedBlocks.copy; + [self.onceVariablesChangedBlocks removeAllObjects]; + } + for (CleverTapVariablesChangedBlock block in onceBlocksCopy) { + block(); + } +} + +- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CleverTap onVariablesChanged]."); + return; + } + + CT_TRY + if (!self.variablesChangedBlocks) { + self.variablesChangedBlocks = [NSMutableArray array]; + } + [self.variablesChangedBlocks addObject:[block copy]]; + CT_END_TRY + + if ([self.varCache hasVarsRequestCompleted]) { + block(); + } +} + +- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CleverTap onceVariablesChanged]."); + return; + } + + if ([self.varCache hasVarsRequestCompleted]) { + block(); + } else { + CT_TRY + static dispatch_once_t onceBlocksToken; + dispatch_once(&onceBlocksToken, ^{ + self.onceVariablesChangedBlocks = [NSMutableArray array]; + }); + @synchronized (self.onceVariablesChangedBlocks) { + [self.onceVariablesChangedBlocks addObject:[block copy]]; + } + CT_END_TRY + } +} + +#pragma mark Vars Payload +- (NSDictionary*)varsPayload { + NSMutableDictionary *payload = [NSMutableDictionary dictionary]; + payload[@"type"] = CT_PE_VARS_PAYLOAD_TYPE; + + NSMutableDictionary *allVars = [NSMutableDictionary dictionary]; + + [self.varCache.vars + enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CTVar * _Nonnull variable, BOOL * _Nonnull stop) { + + NSMutableDictionary *varData = [NSMutableDictionary dictionary]; + + if ([variable.defaultValue isKindOfClass:[NSDictionary class]]) { + NSDictionary *flattenedMap = [self flatten:variable.defaultValue varName:variable.name]; + [allVars addEntriesFromDictionary:flattenedMap]; + } + else { + if ([variable.kind isEqualToString:CT_KIND_INT] || [variable.kind isEqualToString:CT_KIND_FLOAT]) { + varData[CT_PE_VAR_TYPE] = CT_PE_NUMBER_TYPE; + } + else if ([variable.kind isEqualToString:CT_KIND_BOOLEAN]) { + varData[CT_PE_VAR_TYPE] = CT_PE_BOOL_TYPE; + } + else { + varData[CT_PE_VAR_TYPE] = variable.kind; + } + varData[CT_PE_DEFAULT_VALUE] = variable.defaultValue; + allVars[key] = varData; + } + }]; + payload[CT_PE_VARS_PAYLOAD_KEY] = allVars; + + return payload; +} + +- (NSDictionary*)flatten:(NSDictionary*)map varName:(NSString*)varName { + NSMutableDictionary *varsPayload = [NSMutableDictionary dictionary]; + + [map enumerateKeysAndObjectsUsingBlock:^(NSString* _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { + if ([value isKindOfClass:[NSString class]] || + [value isKindOfClass:[NSNumber class]]) { + NSString *flatKey = [NSString stringWithFormat:@"%@.%@", varName, key]; + varsPayload[flatKey] = @{ CT_PE_DEFAULT_VALUE: value }; + } else if ([value isKindOfClass:[NSDictionary class]]) { + NSString *flatKey = [NSString stringWithFormat:@"%@.%@", varName, key]; + NSDictionary* flattenedMap = [self flatten:value varName:flatKey]; + [varsPayload addEntriesFromDictionary:flattenedMap]; + } + }]; + + return varsPayload; +} + +- (NSDictionary*)unflatten:(NSDictionary*)flatDictionary { + if (!flatDictionary) { + return nil; + } + + NSMutableDictionary *unflattenVars = [NSMutableDictionary dictionary]; + [flatDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { + if ([key containsString:@"."]) { + NSArray *components = [self.varCache getNameComponents:key]; + NSMutableDictionary *currentMap = unflattenVars; + NSString *lastComponent = [components lastObject]; + + for (int i = 0; i < components.count - 1; i++) { + NSString *component = components[i]; + if (!currentMap[component]) { + NSMutableDictionary *nestedMap = [NSMutableDictionary dictionary]; + currentMap[component] = nestedMap; + currentMap = nestedMap; + } + else { + currentMap = ((NSMutableDictionary*)currentMap[component]); + } + } + if ([currentMap isKindOfClass:[NSMutableDictionary class]]) { + currentMap[lastComponent] = value; + } + } + else { + unflattenVars[key] = value; + } + }]; + + return unflattenVars; +} + +@end diff --git a/CleverTapSDK/ProductExperiences/ContentMerger.h b/CleverTapSDK/ProductExperiences/ContentMerger.h new file mode 100644 index 00000000..92077a76 --- /dev/null +++ b/CleverTapSDK/ProductExperiences/ContentMerger.h @@ -0,0 +1,13 @@ +// +// ContentMerger.h +// CleverTapSDK +// +// Created by Akash Malhotra on 17/02/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import + +@interface ContentMerger : NSObject ++ (id)mergeWithVars:(id)vars diff:(id)diff; +@end diff --git a/CleverTapSDK/ProductExperiences/ContentMerger.m b/CleverTapSDK/ProductExperiences/ContentMerger.m new file mode 100644 index 00000000..9a391b5e --- /dev/null +++ b/CleverTapSDK/ProductExperiences/ContentMerger.m @@ -0,0 +1,60 @@ +// +// ContentMerger.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 12.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "ContentMerger.h" + +@implementation ContentMerger + ++ (id)mergeWithVars:(id)vars diff:(id)diff { + if (!diff) { + return vars; + } + + // Return the modified value if it is a `primitive` + if ([diff isKindOfClass:[NSNumber class]] || + [diff isKindOfClass:[NSString class]] || + [diff isKindOfClass:[NSNull class]]) { + return diff; + } + if ([vars isKindOfClass:[NSNumber class]] || + [vars isKindOfClass:[NSString class]] || + [vars isKindOfClass:[NSNull class]]) { + return diff; + } + + // Return nil if neither vars nor diff is dictionary. + // Use isKindOfClass: to check first. Note that (NSDictionary *) cast will succeed if object is NSArray*. + if (![vars isKindOfClass:[NSDictionary class]] && ![diff isKindOfClass:[NSDictionary class]]) { + return nil; + } + + if (![vars isKindOfClass:[NSDictionary class]]) { + // diff is dictionary + return diff; + } + + // vars is dictionary + NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:vars]; + if (![diff isKindOfClass:[NSDictionary class]]) { + return merged; + } + + // vars and diff are dictionary + NSDictionary *diffDict = (NSDictionary *)diff; + [diffDict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + id defaultValue = merged[key]; + id mergedValue = [self mergeWithVars:defaultValue diff:value]; + if (mergedValue) { + merged[key] = mergedValue; + } + }]; + + return merged; +} + +@end diff --git a/CleverTapSDK/include/CTVar.h b/CleverTapSDK/include/CTVar.h new file mode 120000 index 00000000..39db957f --- /dev/null +++ b/CleverTapSDK/include/CTVar.h @@ -0,0 +1 @@ +../ProductExperiences/CTVar.h \ No newline at end of file diff --git a/CleverTapSDK/include/CleverTap+CTVar.h b/CleverTapSDK/include/CleverTap+CTVar.h new file mode 120000 index 00000000..efcc8cdb --- /dev/null +++ b/CleverTapSDK/include/CleverTap+CTVar.h @@ -0,0 +1 @@ +../CleverTap+CTVar.h \ No newline at end of file diff --git a/CleverTapSDK/ios.modulemap b/CleverTapSDK/ios.modulemap index 6c796985..7d816fc7 100644 --- a/CleverTapSDK/ios.modulemap +++ b/CleverTapSDK/ios.modulemap @@ -17,5 +17,8 @@ framework module CleverTapSDK { header "CleverTapJSInterface.h" header "CleverTap+InAppNotifications.h" header "CleverTap+SCDomain.h" + header "CTLocalInApp.h" + header "CleverTap+CTVar.h" + header "CTVar.h" export * } diff --git a/CleverTapSDK/tvos.modulemap b/CleverTapSDK/tvos.modulemap index 2f715cfb..bff7197b 100644 --- a/CleverTapSDK/tvos.modulemap +++ b/CleverTapSDK/tvos.modulemap @@ -9,5 +9,7 @@ framework module CleverTapSDK { header "CleverTapUTMDetail.h" header "CleverTapTrackedViewController.h" header "CleverTapInstanceConfig.h" + header "CleverTap+CTVar.h" + header "CTVar.h" export * } diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h new file mode 100644 index 00000000..9f39df79 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h @@ -0,0 +1,20 @@ +// +// CTVarCache+Tests.h +// CleverTapSDKTests +// +// Created by Akash Malhotra on 02/03/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTVarCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTVarCache (Tests) +@property (strong, nonatomic) NSMutableDictionary *valuesFromClient; +- (NSString *)getArchiveFileName; +- (id)traverse:(id)collection withKey:(id)key autoInsert:(BOOL)autoInsert; +- (void)saveDiffs; +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.m b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.m new file mode 100644 index 00000000..bd7748a8 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.m @@ -0,0 +1,21 @@ +// +// CTVarCache+Tests.m +// CleverTapSDKTests +// +// Created by Akash Malhotra on 02/03/23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTVarCache+Tests.h" + +@interface CTVarCache (Tests) +- (NSString*)dataArchiveFileName; +@end + +@implementation CTVarCache (Tests) + +- (NSString *)getArchiveFileName { + return [self dataArchiveFileName]; +} + +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.h b/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.h new file mode 100644 index 00000000..1021e423 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.h @@ -0,0 +1,24 @@ +// +// CTVarCacheMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 29.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTVarCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTVarCacheMock : CTVarCache + +@property int loadCount; +@property int applyCount; +@property int saveCount; + +- (void)originalSaveDiffs; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.m b/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.m new file mode 100644 index 00000000..1a14f98f --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarCacheMock.m @@ -0,0 +1,34 @@ +// +// CTVarCacheMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 29.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTVarCacheMock.h" +#import "CTVarCache+Tests.h" + +@implementation CTVarCacheMock + +- (void)loadDiffs { + self.loadCount++; + [super loadDiffs]; +} + +- (void)applyVariableDiffs:(NSDictionary *)diffs_ { + self.applyCount++; + [super applyVariableDiffs:diffs_]; +} + +- (void)saveDiffs { + // Do NOT save to file + self.saveCount++; +} + +- (void)originalSaveDiffs { + // Save to file + [super saveDiffs]; +} + +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m b/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m new file mode 100644 index 00000000..5e2b5281 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m @@ -0,0 +1,452 @@ +// +// CTVarCacheTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 29.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTVariables.h" +#import "CTVarCache.h" +#import "CTUtils.h" +#import "CTPreferences.h" +#import "CTVarCache+Tests.h" +#import "CTVarCacheMock.h" +#import "CTVariables+Tests.h" +#import "CTConstants.h" + +@interface CTVarCacheTest : XCTestCase + +@end + +CTVariables *variables; + +@implementation CTVarCacheTest + +- (void)setUp { + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"id" accountToken:@"token" accountRegion:@"eu"]; + config.useCustomCleverTapId = YES; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"test"]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo]; + variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; +} + +- (void)tearDown { + variables = nil; +} + +#pragma mark Name Components +- (void)testNameComponents { + NSString *name = @""; + NSArray *components = [[variables varCache] getNameComponents:name]; + XCTAssertEqual(1, [components count]); + XCTAssertEqual(name, components[0]); + + NSString *name1 = @"my var 1"; + NSArray *components1 = [[variables varCache] getNameComponents:name1]; + XCTAssertEqual(1, [components1 count]); + XCTAssertEqual(name1, components1[0]); + + NSString *name2 = @" "; + NSArray *components2 = [[variables varCache] getNameComponents:name2]; + XCTAssertEqual(1, [components2 count]); + XCTAssertEqual(name2, components2[0]); + + NSString *name3 = @"var 2.var4. var 5 "; + NSArray *components3 = [[variables varCache] getNameComponents:name3]; + XCTAssertEqual(3, [components3 count]); + XCTAssertEqualObjects((@[@"var 2", @"var4", @" var 5 "]), components3); + + NSString *name4 = @"&"; + NSArray *components4 = [[variables varCache] getNameComponents:name4]; + XCTAssertEqual(1, [components4 count]); + XCTAssertEqual(name4, components4[0]); + + NSString *name5 = @"var[0]"; + NSArray *components5 = [[variables varCache] getNameComponents:name5]; + XCTAssertEqual(1, [components5 count]); + XCTAssertEqual(name5, components5[0]); + + NSString *component1 = @"first"; + NSString *component2 = @"second"; + NSString *component3 = @"third"; + NSArray *nameComponents = [[variables varCache] getNameComponents:[NSString stringWithFormat:@"%@.%@.%@",component1,component2,component3]]; + XCTAssertNotNil(nameComponents); + XCTAssertEqualObjects((@[component1, component2, component3]), nameComponents); + XCTAssertTrue(nameComponents.count == 3); +} + +#pragma mark Traverse +- (void)testTraverse { + NSDictionary *dictionary = @{ + @"key1": @"value1", + @"key2": @{ + @"nestedKey1": @"nestedValue1", + @"nestedKey2": @"nestedValue2" + } + }; + + // Returns the correct value when the key exists in the dictionary + id result = [variables.varCache traverse:dictionary withKey:@"key1" autoInsert:NO]; + XCTAssertEqualObjects(result, @"value1"); + + // Returns nil when the key does not exist in the dictionary + result = [variables.varCache traverse:dictionary withKey:@"key3" autoInsert:NO]; + XCTAssertNil(result); + + // Returns nil when the value for the key is NSNull + NSDictionary *dictionaryWithNull = @{ + @"key1": [NSNull null] + }; + result = [variables.varCache traverse:dictionaryWithNull withKey:@"key1" autoInsert:NO]; + XCTAssertNil(result); +} + +- (void)testTraverseWithAutoInsert { + NSDictionary *dictionary = @{ + @"key1": @"value1", + @"key2": @{ + @"nestedKey1": @"nestedValue1", + @"nestedKey2": @"nestedValue2" + } + }; + + // Creates a new dictionary and adds it to the collection when autoInsert is true and the key does not exist + NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary]; + [variables.varCache traverse:mutableDictionary withKey:@"newKey" autoInsert:YES]; + XCTAssertTrue([mutableDictionary objectForKey:@"newKey"] != nil); + XCTAssertTrue([[mutableDictionary objectForKey:@"newKey"] isKindOfClass:[NSMutableDictionary class]]); + + // Does not create a new dictionary when autoInsert is false and the key does not exist + [variables.varCache traverse:mutableDictionary withKey:@"newKey2" autoInsert:NO]; + XCTAssertNil([mutableDictionary objectForKey:@"newKey2"]); +} + +#pragma mark Register Variables +- (void)testRegisterVars { + CTVar *var = [variables define:@"test" with:@"test" kind:CT_KIND_STRING]; + XCTAssertEqual(variables.varCache.vars[var.name], var); +} + +- (void)testRegisterVariableWithGroup { + [variables define:@"group.var1" with:@"value1" kind:CT_KIND_STRING]; + [variables define:@"group" with:@{ + @"var2": @"value2" + } kind:CT_KIND_DICTIONARY]; + + NSDictionary *expectedGroupDefaultValue = @{ @"var2": @"value2" }; + NSDictionary *expectedGroupValue = @{ @"var1": @"value1", @"var2": @"value2" }; + + CTVarCache *varCache = variables.varCache; + XCTAssertEqual(2, varCache.vars.count); + XCTAssertEqualObjects(@"value1", [varCache getVariable:@"group.var1"].defaultValue); + XCTAssertEqualObjects(@"value1", [varCache getVariable:@"group.var1"].value); + XCTAssertEqualObjects(expectedGroupDefaultValue, [varCache getVariable:@"group"].defaultValue); + XCTAssertEqualObjects(expectedGroupValue, [varCache getVariable:@"group"].value); +} + +- (void)testRegisterVariableWithNestedGroup { + [variables define:@"group1.var1" with:@1 kind:CT_KIND_INT]; + [variables define:@"group1.group2.var3" with:@NO kind:CT_KIND_BOOLEAN]; + NSDictionary *group1DefaultValue = @{ + @"var2": @2, + @"group2": @{ + @"var4": @4, + } + }; + [variables define:@"group1" with:group1DefaultValue kind:CT_KIND_DICTIONARY]; + + NSDictionary *expectedGroup1Value = @{ + @"var1": @1, + @"var2": @2, + @"group2": @{ + @"var4": @4, + @"var3": @NO, + } + }; + + CTVarCache *varCache = variables.varCache; + XCTAssertEqual(3, varCache.vars.count); + XCTAssertEqualObjects(@1, [varCache getVariable:@"group1.var1"].value); + XCTAssertEqualObjects(@NO, [varCache getVariable:@"group1.group2.var3"].value); + + XCTAssertEqualObjects(group1DefaultValue, [varCache getVariable:@"group1"].defaultValue); + XCTAssertEqualObjects(expectedGroup1Value, [varCache getVariable:@"group1"].value); +} + +#pragma mark Get Merged Value +- (void)testGetMergedValue { + NSString *varName = @"var1"; + [variables define:varName with:@"value1" kind:CT_KIND_STRING]; + NSString *value = [variables.varCache getMergedValue:varName]; + + XCTAssertEqualObjects(@"value1", value); +} + +- (void)testGetMergedValueWithGroup { + [variables define:@"group1.var1" with:@"value1" kind:CT_KIND_STRING]; + [variables define:@"group1.group2.var3" with:@NO kind:CT_KIND_BOOLEAN]; + [variables define:@"group1" with:@{ + @"var2": @2, + @"group2": @{ + @"var4": @4, + } + } kind:CT_KIND_DICTIONARY]; + + NSDictionary *expectedGroup1Value = @{ + @"var1": @"value1", + @"var2": @2, + @"group2": @{ + @"var4": @4, + @"var3": @NO, + } + }; + + XCTAssertEqualObjects(expectedGroup1Value, [variables.varCache getMergedValue:@"group1"]); +} + +- (void)testGetMergedValueWithGroups { + [variables define:@"group1.var1" with:@"value1" kind:CT_KIND_STRING]; + [variables define:@"group1.group2.var3" with:@NO kind:CT_KIND_BOOLEAN]; + [variables define:@"group1" with:@{ + @"var2": @2, + @"group2": @{ + @"var4": @4, + } + } kind:CT_KIND_DICTIONARY]; + + [variables define:@"var5" with:@"value5" kind:CT_KIND_STRING]; + + XCTAssertEqualObjects(@"value1", [variables.varCache getMergedValue:@"group1.var1"]); + XCTAssertEqualObjects(@2, [variables.varCache getMergedValue:@"group1.var2"]); + XCTAssertEqualObjects(@NO, [variables.varCache getMergedValue:@"group1.group2.var3"]); + XCTAssertEqualObjects(@4, [variables.varCache getMergedValue:@"group1.group2.var4"]); + + XCTAssertEqualObjects(@"value5", [variables.varCache getMergedValue:@"var5"]); +} + +#pragma mark Get Variable +- (void)testGetVariable { + NSString *varName = @"var"; + CTVar *var = [variables define:varName with:@"value" kind:CT_KIND_STRING]; + CTVar *varResult = [variables.varCache getVariable:varName]; + + XCTAssertEqual(varResult, var); +} + +- (void)testGetVariableGroup { + NSString *varName = @"group.var"; + CTVar *var = [variables define:varName with:@"value" kind:CT_KIND_STRING]; + CTVar *varResult = [variables.varCache getVariable:varName]; + CTVar *varGroupResult = [variables.varCache getVariable:@"group"]; + + XCTAssertEqual(varResult, var); + XCTAssertNil(varGroupResult); + + CTVar *varDict = [variables define:@"dict" with:@{} kind:CT_KIND_DICTIONARY]; + CTVar *varDictResult = [variables.varCache getVariable:@"dict"]; + XCTAssertEqual(varDictResult, varDict); +} + +#pragma mark Merge Variable +- (void)testMergeVariable { + NSDictionary *diffs = @{ + @"var1": @2, + @"group1": @{ + @"var1": @"value2", + @"group2": @{ + @"var2": @YES, + } + } + }; + // Apply diffs first, before defining the variable + [variables.varCache applyVariableDiffs:diffs]; + + CTVar *var1 = [variables define:@"var1" with:@1 kind:CT_KIND_INT]; + CTVar *group1_var1 = [variables define:@"group1.var1" with:@"value1" kind:CT_KIND_STRING]; + CTVar *group1_group2 = [variables define:@"group1.group2" with:@{ + @"var2": @NO, + @"var3": @3, + } kind:CT_KIND_DICTIONARY]; + + XCTAssertEqual(@2, var1.value); + XCTAssertEqual(@"value2", group1_var1.value); + XCTAssertEqualObjects((@{ @"var2": @YES, @"var3": @3 }), group1_group2.value); +} + +#pragma mark Apply Diffs +- (void)testApplyDiffs { + CTVar *var1 = [variables define:@"var1" with:@1 kind:CT_KIND_INT]; + CTVar *group1_var1 = [variables define:@"group1.var1" with:@"value1" kind:CT_KIND_STRING]; + CTVar *var3 = [variables define:@"group1.group2.var3" with:@NO kind:CT_KIND_BOOLEAN]; + + NSDictionary *diffs = @{ + @"var1": @2, + @"group1": @{ + @"var1": @"value2", + @"var22": @"value22", + @"group2": @{ + @"var3": @YES, + } + } + }; + + [variables.varCache applyVariableDiffs:diffs]; + + XCTAssertEqualObjects(@2, var1.value); + XCTAssertEqualObjects(@"value2", group1_var1.value); + XCTAssertEqualObjects(@YES, var3.value); + XCTAssertEqualObjects(@"value22", [variables.varCache getMergedValue:@"group1.var22"]); +} + +- (void)testApplyDiffsDefaultValue { + CTVar *var1 = [variables define:@"var1" with:@1 kind:CT_KIND_INT]; + CTVar *group1_var1 = [variables define:@"group1.var1" with:@"value1" kind:CT_KIND_STRING]; + CTVar *var3 = [variables define:@"group1.group2.var3" with:@NO kind:CT_KIND_BOOLEAN]; + + NSDictionary *diffs = @{ + @"group1": @{ + @"var22": @"value22", + } + }; + + [variables.varCache applyVariableDiffs:diffs]; + + XCTAssertEqualObjects(@1, var1.value); + XCTAssertEqualObjects(@"value1", group1_var1.value); + XCTAssertEqualObjects(@NO, var3.value); + XCTAssertEqualObjects(@"value22", [variables.varCache getMergedValue:@"group1.var22"]); +} + +- (void)testApplyDiffsGroup { + CTVar *var1 = [variables define:@"group1.group2.var1" with:@1 kind:CT_KIND_INT]; + CTVar *group1_group2 = [variables define:@"group1.group2" with:@{ + @"var2": @"value2" + } kind:CT_KIND_DICTIONARY]; + + NSDictionary *diffs = @{ + @"group1": @{ + @"group2": @{ + @"var3": @"value3" + } + } + }; + + [variables.varCache applyVariableDiffs:diffs]; + + NSDictionary *group1_group2_value = @{ + @"var1": @1, + @"var2": @"value2", + @"var3": @"value3" + }; + + XCTAssertEqualObjects(@1, var1.value); + XCTAssertEqualObjects(group1_group2_value, group1_group2.value); + XCTAssertEqualObjects(@"value3", [variables.varCache getMergedValue:@"group1.group2.var3"]); +} + +#pragma mark Save Diffs +- (void)testLoadSaveSpecialCharacters { + CTVar *var1 = [variables define:@"&" with:@"&" kind:CT_KIND_STRING]; + CTVar *var2 = [variables define:@"[var]" with:@"[var]" kind:CT_KIND_STRING]; + CTVar *var3 = [variables define:@" " with:@" " kind:CT_KIND_STRING]; + + CTVar *var4 = [variables define:@"<<" with:@"<<" kind:CT_KIND_STRING]; + CTVar *var5 = [variables define:@"and&" with:@"and&" kind:CT_KIND_STRING]; + CTVar *var6 = [variables define:@"'a" with:@"'a" kind:CT_KIND_STRING]; + + CTVarCacheMock *varCache = (CTVarCacheMock *)variables.varCache; + + NSDictionary *diffs = @{ + @"&": @"&", + @"[var]": @"[var2]", + @" ": @" ", + @"<<": @"<<<", + @"and&": @"b&", + @"'a": @"'b" + }; + + [varCache applyVariableDiffs:diffs]; + // Call original saveDiffs and write to file + [varCache originalSaveDiffs]; + [varCache loadDiffs]; + + XCTAssertEqualObjects(@"&", var1.stringValue); + XCTAssertEqualObjects(@"[var2]", var2.stringValue); + XCTAssertEqualObjects(@" ", var3.stringValue); + XCTAssertEqualObjects(@"<<<", var4.stringValue); + XCTAssertEqualObjects(@"b&", var5.stringValue); + XCTAssertEqualObjects(@"'b", var6.stringValue); + + [self deleteSavedFile:[variables.varCache getArchiveFileName]]; +} + +- (void)testSavesDiffs { + CTVarCacheMock *varCache = (CTVarCacheMock *)variables.varCache; + [variables define:@"var1" with:@"value" kind:CT_KIND_STRING]; + + NSDictionary *diff = @{ + @"var1": @"new value", + }; + [variables handleVariablesResponse:diff]; + + XCTAssertEqual(1, varCache.saveCount); + XCTAssertTrue([varCache hasVarsRequestCompleted]); + + // Call original saveDiffs and write to file + [varCache originalSaveDiffs]; + + NSString *fileName = [varCache getArchiveFileName]; + NSString *filePath = [CTPreferences filePathfromFileName:fileName]; + NSData *diffsData = [NSData dataWithContentsOfFile:filePath]; + NSKeyedUnarchiver *unarchiver; + NSError *error = nil; + if (@available(iOS 12.0, *)) { + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:diffsData error:&error]; + XCTAssertNil(error); + unarchiver.requiresSecureCoding = NO; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:diffsData]; +#pragma clang diagnostic pop + } + NSDictionary *loadedVars = (NSDictionary *) [unarchiver decodeObjectForKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + XCTAssertTrue([diff isEqualToDictionary:loadedVars]); + + [self deleteSavedFile:fileName]; +} + +- (void)testLoadsDiffs { + NSString *varName = @"var1"; + NSString *initialValue = @"value"; + NSString *overriddenValue = @"overridden"; + CTVarCacheMock *varCache = (CTVarCacheMock *)variables.varCache; + + [variables define:varName with:initialValue kind:CT_KIND_STRING]; + + NSDictionary *diff = @{ + varName: overriddenValue, + }; + [variables handleVariablesResponse:diff]; + XCTAssertEqual(1, varCache.saveCount); + XCTAssertTrue([varCache hasVarsRequestCompleted]); + + // Call original saveDiffs and write to file + [varCache originalSaveDiffs]; + + [varCache loadDiffs]; + CTVar *loadedVar = varCache.vars[varName]; + XCTAssertEqualObjects(loadedVar.value, overriddenValue); + + [self deleteSavedFile:[varCache getArchiveFileName]]; +} + +- (void)deleteSavedFile:(NSString *)fileName { + NSString *filePath = [CTPreferences filePathfromFileName:fileName]; + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; +} + +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVarTest.m b/CleverTapSDKTests/ProductExperiences/CTVarTest.m new file mode 100644 index 00000000..c02777df --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVarTest.m @@ -0,0 +1,228 @@ +// +// CTVarTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.04.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import +#import "CTVariables.h" +#import "CTVarCache.h" +#import "CTVarCacheMock.h" +#import "CTConstants.h" +#import "CTVariables+Tests.h" + +@interface CTVarDelegateImpl : NSObject + +typedef void(^Callback)(CTVar *); +@property Callback callback; + +@end + +@implementation CTVarDelegateImpl + +- (void)valueDidChange:(CTVar *)variable { + if ([self callback]) { + self.callback(variable); + } +} + +@end + +@interface CTVarTest : XCTestCase + +@property(strong, nonatomic) CTVariables *variables; + +@end + +@implementation CTVarTest + +- (void)setUp { + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"id" accountToken:@"token" accountRegion:@"eu"]; + config.useCustomCleverTapId = YES; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"test"]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo]; + self.variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; +} + +- (void)tearDown { + self.variables = nil; +} + +- (void)testVariableName { + CTVar *var1 = [self.variables define:@"&" with:@"&" kind:CT_KIND_STRING]; + CTVar *var2 = [self.variables define:@"[var]" with:@"[var]" kind:CT_KIND_STRING]; + CTVar *var3 = [self.variables define:@" " with:@" " kind:CT_KIND_STRING]; + + CTVar *var4 = [self.variables define:@".group.var." with:@"value1" kind:CT_KIND_STRING]; + CTVar *var5 = [self.variables define:@"" with:@"" kind:CT_KIND_STRING]; + + XCTAssertEqualObjects(@"&", var1.name); + XCTAssertEqualObjects(@"[var]", var2.name); + XCTAssertEqualObjects(@" ", var3.name); + + XCTAssertNil(var4); + XCTAssertNil(var5); +} + +- (void)testCTVarDelegate { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; + __block CTVar *varFromDelegate = nil; + [del setCallback:^(CTVar * variable) { + varFromDelegate = variable; + [expect fulfill]; + }]; + [var1 setDelegate:del]; + + NSDictionary *diffs = @{ + @"var1": @2, + }; + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqual(@"var1", varFromDelegate.name); + XCTAssertEqual(@2, varFromDelegate.value); +} + +- (void)testCTVarDelegateOnError { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; + __block CTVar *varFromDelegate = nil; + [del setCallback:^(CTVar * variable) { + varFromDelegate = variable; + [expect fulfill]; + }]; + [var1 setDelegate:del]; + + [self.variables handleVariablesError]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqual(@"var1", varFromDelegate.name); + XCTAssertEqual(@1, varFromDelegate.value); +} + +- (void)testCTVarDelegateAfterStart { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; + __block CTVar *varFromDelegate = nil; + [del setCallback:^(CTVar * variable) { + varFromDelegate = variable; + [expect fulfill]; + }]; + + NSDictionary *diffs = @{ + @"var1": @2, + }; + [self.variables handleVariablesResponse:diffs]; + + // Set delegate after handling response + [var1 setDelegate:del]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqual(@"var1", varFromDelegate.name); + XCTAssertEqual(@2, varFromDelegate.value); +} + +- (void)testOnValueChangedNoDiff { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [var1 onValueChanged:^{ + [expect fulfill]; + }]; + + [self.variables handleVariablesResponse:@{}]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqualObjects(@1, var1.value); +} + +- (void)testOnValueChangedDiff { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [var1 onValueChanged:^{ + [expect fulfill]; + }]; + + NSDictionary *diffs = @{ + @"var1": @2, + }; + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqual(@2, var1.value); +} + +- (void)testOnValueChangedOnError { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [var1 onValueChanged:^{ + [expect fulfill]; + }]; + [self.variables handleVariablesError]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqual(@1, var1.value); +} + +- (void)testHadStarted { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + XCTAssertFalse(var1.hadStarted); + self.variables.varCache.hasVarsRequestCompleted = @YES; + [self.variables.varCache applyVariableDiffs:@{}]; + XCTAssertTrue(var1.hadStarted); +} + +- (void)testHasChanged { + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + self.variables.varCache.hasVarsRequestCompleted = @YES; + [self.variables.varCache applyVariableDiffs:@{}]; + XCTAssertFalse(var1.hasChanged); + + NSDictionary *diffs = @{ + @"var1": @2, + }; + [self.variables.varCache applyVariableDiffs:diffs]; + XCTAssertTrue(var1.hasChanged); +} + +- (void)testVarValues { + NSNumber *varValue = [NSNumber numberWithDouble:6.67345983745897]; + CTVar *var = [self.variables define:@"varNumber" with:varValue kind:CT_KIND_FLOAT]; + + XCTAssertEqualObjects(var.stringValue,varValue.stringValue); + XCTAssertEqual(var.floatValue,varValue.floatValue); + XCTAssertEqual(var.intValue,varValue.intValue); + XCTAssertEqual(var.integerValue,varValue.integerValue); + XCTAssertEqual(var.doubleValue,varValue.doubleValue); + XCTAssertEqual(var.boolValue,varValue.boolValue); + XCTAssertEqual(var.longValue,varValue.longValue); + XCTAssertEqual(var.longLongValue,varValue.longLongValue); + XCTAssertEqual(var.unsignedIntValue,varValue.unsignedIntValue); + XCTAssertEqual(var.unsignedLongValue,varValue.unsignedLongValue); + XCTAssertEqual(var.unsignedIntegerValue,varValue.unsignedIntegerValue); + XCTAssertEqual(var.shortValue,varValue.shortValue); + XCTAssertEqual(var.unsignedShortValue,varValue.unsignedShortValue); + XCTAssertEqual(var.unsignedLongLongValue,varValue.unsignedLongLongValue); + XCTAssertEqual(var.cgFloatValue,varValue.doubleValue); + XCTAssertEqual(var.charValue,varValue.charValue); + XCTAssertEqual(var.unsignedCharValue,varValue.unsignedCharValue); + + CTVar *groupVar = [self.variables define:@"group" with:@{@"number":varValue} kind:CT_KIND_DICTIONARY]; + XCTAssertTrue([[groupVar objectForKey:@"number"] isKindOfClass:[varValue class]]); + XCTAssertTrue([groupVar.value isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([groupVar.defaultValue isKindOfClass:[NSDictionary class]]); +} + +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.h b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.h new file mode 100644 index 00000000..b06dbb2c --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.h @@ -0,0 +1,16 @@ +// +// CTVariables+Tests.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 29.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import "CTVariables.h" +#import "CTVarCacheMock.h" + +@interface CTVariables (Tests) + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo *)deviceInfo varCache: (CTVarCacheMock *)varCache; +- (void)triggerVariablesChanged; +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m new file mode 100644 index 00000000..0542d2c8 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m @@ -0,0 +1,22 @@ +// +// CTVariables+Tests.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 29.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import "CTVariables+Tests.h" + +@implementation CTVariables (Tests) + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo *)deviceInfo varCache: (CTVarCacheMock *)varCache { + self = [super init]; + if (self) { + self.varCache = varCache; + } + return self; +} + +@end diff --git a/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m b/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m new file mode 100644 index 00000000..1ae58173 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m @@ -0,0 +1,579 @@ +// +// CTVariablesTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 26.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import +#import "CTVariables+Tests.h" +#import "CTVarCacheMock.h" +#import "CTVariables.h" +#import "CTConstants.h" + +@interface CTVariablesTest : XCTestCase + +@property(strong, nonatomic) CTVariables *variables; + +@end + +@implementation CTVariablesTest + +- (void)setUp { + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"id" accountToken:@"token" accountRegion:@"eu"]; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"test"]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo]; + self.variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; +} + +- (void)tearDown { + self.variables = nil; +} + +- (void)testVarCacheNotNil { + XCTAssertNotNil(self.variables.varCache); +} + +#pragma mark Sync Variables +- (void)testSyncVars { + NSString *varName = @"var1"; + NSString *varValue = @"value1"; + + CTVar *definedVar = [self.variables define:varName with:varValue kind:CT_KIND_STRING]; + + NSDictionary *payload = [self.variables varsPayload]; + + XCTAssertEqualObjects(payload[@"type"], CT_PE_VARS_PAYLOAD_TYPE); + NSDictionary *vars = [payload objectForKey:CT_PE_VARS_PAYLOAD_KEY]; + NSDictionary *titleMap = [vars objectForKey:varName]; + XCTAssertEqualObjects(titleMap[CT_PE_DEFAULT_VALUE], varValue); + XCTAssertEqualObjects(titleMap[CT_PE_VAR_TYPE], definedVar.kind); +} + +- (void)testSyncVarsComplex { + [self.variables define:@"var1" with:@"value1" kind:CT_KIND_STRING]; + [self.variables define:@"var2.var22" with:@"value2" kind:CT_KIND_STRING]; + [self.variables define:@"var3" with:@YES kind:CT_KIND_BOOLEAN]; + [self.variables define:@"var4" with:@1234 kind:CT_KIND_INT]; + [self.variables define:@"var5" with:@12.34 kind:CT_KIND_FLOAT]; + [self.variables define:@"var6" with:@{ + @"var7": @"value7", + @"var8": @"value8" + } kind:CT_KIND_DICTIONARY]; + + NSDictionary *expected = @{ + @"type": @"varsPayload", + @"vars": @{ + @"var1": @{ + @"defaultValue": @"value1", + @"type": @"string" + }, + @"var2.var22": @{ + @"defaultValue": @"value2", + @"type": @"string" + }, + @"var3": @{ + @"defaultValue": @1, + @"type": @"boolean" + }, + @"var4": @{ + @"defaultValue": @1234, + @"type": @"number" + }, + @"var5": @{ + @"defaultValue": @12.34, + @"type": @"number" + }, + @"var6.var7": @{ + @"defaultValue": @"value7", + }, + @"var6.var8": @{ + @"defaultValue": @"value8", + }, + } + }; + + NSDictionary *actual = [self.variables varsPayload]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testSyncVarsWithDots { + [self.variables define:@"var1.var2" with:@"value2" kind:CT_KIND_STRING]; + [self.variables define:@"var1.var3" with:@"value3" kind:CT_KIND_STRING]; + [self.variables define:@"var1.var4.var5" with:@YES kind:CT_KIND_BOOLEAN]; + [self.variables define:@"var1.var4.var6" with:@1234 kind:CT_KIND_INT]; + [self.variables define:@"var7.var8" with:@12.34 kind:CT_KIND_FLOAT]; + + NSDictionary *expected = @{ + @"type": @"varsPayload", + @"vars": @{ + @"var1.var2": @{ + @"defaultValue": @"value2", + @"type": @"string" + }, + @"var1.var3": @{ + @"defaultValue": @"value3", + @"type": @"string" + }, + @"var1.var4.var5": @{ + @"defaultValue": @1, + @"type": @"boolean" + }, + @"var1.var4.var6": @{ + @"defaultValue": @1234, + @"type": @"number" + }, + @"var7.var8": @{ + @"defaultValue": @12.34, + @"type": @"number" + } + } + }; + + NSDictionary *actual = [self.variables varsPayload]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testSyncVarsWithDotsAndDictionaries { + [self.variables define:@"var1.var2" with:@"value2" kind:CT_KIND_STRING]; + [self.variables define:@"var1.var4" with:@{ + @"var5": @NO, + @"var6": @1234 + } kind:CT_KIND_DICTIONARY]; + [self.variables define:@"var7.var8" with:@"value8" kind:CT_KIND_STRING]; + [self.variables define:@"var7" with:@{ + @"var9": @12.34 + } kind:CT_KIND_DICTIONARY]; + [self.variables define:@"var7.var10" with:@"value10" kind:CT_KIND_STRING]; + + + NSDictionary *expected = @{ + @"type": @"varsPayload", + @"vars": @{ + @"var1.var2": @{ + @"defaultValue": @"value2", + @"type": @"string" + }, + @"var1.var4.var5": @{ + @"defaultValue": @0 + }, + @"var1.var4.var6": @{ + @"defaultValue": @1234 + }, + @"var7.var8": @{ + @"defaultValue": @"value8", + @"type": @"string" + }, + @"var7.var9": @{ + @"defaultValue": @12.34 + }, + @"var7.var10": @{ + @"defaultValue": @"value10", + @"type": @"string" + } + } + }; + + NSDictionary *actual = [self.variables varsPayload]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testSyncVarsWithInvalidArray { + [self.variables define:@"var1" with:@"value1" kind:CT_KIND_STRING]; + [self.variables define:@"var2" with:@{ + @"var3": @[ @"arr" ], + @"var4": @"value4" + } kind:CT_KIND_DICTIONARY]; + + // The array will be dropped + NSDictionary *expected = @{ + @"type": @"varsPayload", + @"vars": @{ + @"var1": @{ + @"defaultValue": @"value1", + @"type": @"string" + }, + @"var2.var4": @{ + @"defaultValue": @"value4" + } + } + }; + + NSDictionary *actual = [self.variables varsPayload]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testSyncVarsWithEmpty { + NSDictionary *expected = @{ + @"type": @"varsPayload", + @"vars": @{} + }; + + NSDictionary *actual = [self.variables varsPayload]; + XCTAssertEqualObjects(actual, expected); +} + +#pragma mark Unflatten Variables +- (void)testUnflattenVariables { + NSDictionary *flat = @{ + @"a.b.c.d": @"d value", + @"a.b.c.dd": @"dd value", + @"a.e": @"e value", + @"a.b.bb": @"bb value", + }; + NSDictionary *expected = @{ + @"a": @{ + @"b": @{ + @"c": @{ + @"d": @"d value", + @"dd": @"dd value" + }, + @"bb": @"bb value" + }, + @"e": @"e value" + } + }; + NSDictionary *result = [self.variables unflatten:flat]; + XCTAssertEqualObjects(result, expected); +} + +- (void)testUnflattenWithFlatInput { + NSDictionary *inputDict = @{ + @"a": @"value1", + @"b": @123 + }; + + NSDictionary *expectedOutput = @{ + @"a": @"value1", + @"b": @123 + }; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testUnflattenWithDictionaryInput { + NSDictionary *inputDict = @{ + @"testVarName.a.b": @{ + @"defaultValue": @"value1" + }, + @"testVarName.a.c.d": @{ + @"defaultValue": @"value2" + }, + @"testVarName.e": @{ + @"defaultValue": @"value3" + }, + @"testVarName.f": @{ + @"defaultValue": @"value4" + } + }; + + NSDictionary *expectedOutput = @{ + @"testVarName": @{ + @"a": @{ + @"b": @{ + @"defaultValue": @"value1" + }, + @"c": @{ + @"d": @{ + @"defaultValue": @"value2" + } + } + }, + @"e": @{ + @"defaultValue": @"value3" + }, + @"f": @{ + @"defaultValue": @"value4" + } + } + }; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testUnflattenWithEmptyInput { + NSDictionary *inputDict = @{}; + + NSDictionary *expectedOutput = @{}; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testUnflattenWithInvalidDictionary { + NSDictionary *inputDict = @{ + @"a.b.c.d": @"d value", + @"a.b.c": @"c value", + @"a.e": @"e value", + @"a.b": @"b value", + }; + + NSDictionary *expectedOutput = @{ + @"a": @{ + @"b": @"b value", + @"e": @"e value" + } + }; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testUnflattenWithInvalidDictionaryDifferentOrder { + NSDictionary *inputDict = @{ + @"a.b.c": @"c value", + @"a.b.c.d": @"d value", + @"a.e": @"e value", + @"a.b": @"b value", + }; + + NSDictionary *expectedOutput = @{ + @"a": @{ + @"b": @"b value", + @"e": @"e value" + } + }; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testUnflattenWithInvalidInputArray { + NSDictionary *inputDict = @{ + @"a": @[ @"value2" ] + }; + + NSDictionary *expectedOutput = @{ + @"a": @[ @"value2" ] + }; + + NSDictionary *actualOutput = [self.variables unflatten:inputDict]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +#pragma mark Flatten Variables + +- (void)testFlatten { + NSDictionary *inputDict = @{ + @"Team": @{ + @"TeamName": @"Testing", + @"Designation": @"Tester" + }, + @"Name": @"CleverTap", + @"EmployeeID": @123 + }; + + NSString *varName = @"Employee"; + NSDictionary *expected = @{ + @"Employee.Team.TeamName": @{ + @"defaultValue": @"Testing" + }, + @"Employee.Team.Designation": @{ + @"defaultValue": @"Tester" + }, + @"Employee.Name": @{ + @"defaultValue": @"CleverTap" + }, + @"Employee.EmployeeID": @{ + @"defaultValue": @123 + } + }; + + NSDictionary *result = [self.variables flatten:inputDict varName:varName]; + XCTAssertEqualObjects(result, expected); +} + +- (void)testFlattenWithMultipleDictionaries { + NSDictionary *inputDict = @{ + @"a": @{ + @"b": @"value1", + @"c": @{ + @"d": @"value2", + @"e": @{ + @"f": @"value3", + @"g": @"value4" + }, + @"h": @"value5" + }, + @"i": @"value6" + }, + @"j": @"value7", + @"k": @{ + @"l": @"value8", + @"m": @{ + @"n": @"value9" + } + } + }; + + NSString *varName = @"testVarName"; + NSDictionary *expected = @{ + @"testVarName.a.b": @{ + @"defaultValue": @"value1" + }, + @"testVarName.a.c.d": @{ + @"defaultValue": @"value2" + }, + @"testVarName.a.c.e.f": @{ + @"defaultValue": @"value3" + }, + @"testVarName.a.c.e.g": @{ + @"defaultValue": @"value4" + }, + @"testVarName.a.c.h": @{ + @"defaultValue": @"value5" + }, + @"testVarName.a.i": @{ + @"defaultValue": @"value6" + }, + @"testVarName.j": @{ + @"defaultValue": @"value7" + }, + @"testVarName.k.l": @{ + @"defaultValue": @"value8" + }, + @"testVarName.k.m.n": @{ + @"defaultValue": @"value9" + }, + }; + + NSDictionary *actual = [self.variables flatten:inputDict varName:varName]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testFlattenWithEmptyInput { + NSDictionary *inputDict = @{}; + + NSString *varName = @"testVarName"; + NSDictionary *expectedOutput = @{}; + + NSDictionary *actualOutput = [self.variables flatten:inputDict varName:varName]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +- (void)testFlattenWithInvalidInputArray { + NSDictionary *inputDict = @{ + @"a": @"value1", + @"b": @123, + @"c": @[ @"value2" ] + }; + + NSString *varName = @"testVarName"; + // The array will be dropped + NSDictionary *expectedOutput = @{ + @"testVarName.a": @{ + @"defaultValue": @"value1" + }, + @"testVarName.b": @{ + @"defaultValue": @123 + } + }; + + NSDictionary *actualOutput = [self.variables flatten:inputDict varName:varName]; + XCTAssertEqualObjects(actualOutput, expectedOutput); +} + +#pragma mark Handle Response +- (void)testHandleVariablesResponse { + XCTAssertFalse([self.variables.varCache hasVarsRequestCompleted]); + [self.variables handleVariablesResponse:@{}]; + XCTAssertTrue([self.variables.varCache hasVarsRequestCompleted]); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache loadCount] == 0); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache applyCount] == 1); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache saveCount] == 1); + + [self.variables handleVariablesResponse:@{}]; + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache loadCount] == 0); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache applyCount] == 2); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache saveCount] == 2); +} + +- (void)testHandleVariablesError { + XCTAssertFalse([self.variables.varCache hasVarsRequestCompleted]); + [self.variables handleVariablesError]; + XCTAssertTrue([self.variables.varCache hasVarsRequestCompleted]); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache loadCount] == 1); + XCTAssertTrue([(CTVarCacheMock *)self.variables.varCache applyCount] == 1); +} + +- (void)testClearContent { + CTVar *var = [self.variables define:@"var1" with:@"value" kind:CT_KIND_STRING]; + XCTAssertFalse([self.variables.varCache hasVarsRequestCompleted]); + [self.variables handleVariablesResponse:@{ @"var1": @"new value"}]; + XCTAssertTrue([self.variables.varCache hasVarsRequestCompleted]); + XCTAssertTrue([var hadStarted]); + XCTAssertTrue([var hasChanged]); + + [self.variables clearUserContent]; + + XCTAssertFalse([var hadStarted]); + XCTAssertFalse([var hasChanged]); +} + +#pragma mark Callbacks +- (void)testOnVariablesChanged { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + XCTestExpectation *expect2 = [self expectationWithDescription:@"delegate2"]; + + [self.variables onVariablesChanged:^{ + [expect fulfill]; + }]; + [self.variables onVariablesChanged:^{ + [expect2 fulfill]; + }]; + [self.variables handleVariablesResponse:@{}]; + + [self waitForExpectations:@[expect, expect2] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testOnceVariablesChanged { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [self.variables onceVariablesChanged:^{ + // Should be called once + [expect fulfill]; + }]; + // Call twice to trigger callbacks two times + [self.variables handleVariablesResponse:@{}]; + [self.variables handleVariablesResponse:@{}]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testOnVariablesChangedOnError { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [self.variables onVariablesChanged:^{ + [expect fulfill]; + }]; + [self.variables handleVariablesError]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testFetchVariables { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [self.variables setFetchVariablesBlock:^(BOOL success) { + XCTAssertTrue(success); + [expect fulfill]; + }]; + [self.variables handleVariablesResponse:@{}]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testFetchVariablesOnError { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + [self.variables setFetchVariablesBlock:^(BOOL success) { + XCTAssertFalse(success); + [expect fulfill]; + }]; + [self.variables handleVariablesError]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +@end + diff --git a/CleverTapSDKTests/ProductExperiences/ContentMergerTest.m b/CleverTapSDKTests/ProductExperiences/ContentMergerTest.m new file mode 100644 index 00000000..46ddfda2 --- /dev/null +++ b/CleverTapSDKTests/ProductExperiences/ContentMergerTest.m @@ -0,0 +1,413 @@ +// +// ContentMergerTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 23.03.23. +// Copyright © 2023 CleverTap. All rights reserved. +// + +#import +#import +#import "ContentMerger.h" + +@interface ContentMergerTest : XCTestCase + +@end + +@implementation ContentMergerTest + +- (void)testMergePrimitive { + NSString *vars = @"a"; + NSNumber *diff = @4; + NSNumber *result = (NSNumber *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertEqualObjects(diff, result); +} + +- (void)testMergeBool { + NSNumber *vars = @YES; + NSNumber *diff = @NO; + NSNumber *result = (NSNumber *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertEqualObjects(diff, result); + XCTAssertFalse([result boolValue]); +} + +- (void)testMergeBoolYes { + NSNumber *vars = @NO; + NSNumber *diff = @YES; + NSNumber *result = (NSNumber *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertEqualObjects(diff, result); + XCTAssertTrue([result boolValue]); +} + +- (void)testMergeMapWithPrimitive { + NSDictionary *vars = @{ + @"abc": @"qwe", + @"1": @123 + }; + NSString *diff = @"diff"; + NSString *result = (NSString *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertEqualObjects(diff, result); +} + +- (void)testMergeValues { + NSString *resultString = (NSString *)[ContentMerger mergeWithVars:@"defaultValue" diff:@"newValue"]; + XCTAssertEqualObjects(@"newValue", resultString); + + NSNumber *resultNumber = (NSNumber *)[ContentMerger mergeWithVars:@199 diff:@123456789]; + XCTAssertEqualObjects(@123456789, resultNumber); + + NSString *resultNull = (NSString *)[ContentMerger mergeWithVars:[NSNull null] diff:@"newValue"]; + XCTAssertEqualObjects(@"newValue", resultNull); +} + +- (void)testMergeValuesComplex { + NSDictionary *vars = @{ + @"messageId1": @{ + @"vars": @{ + @"myNumber": @0, + @"myString": @"defaultValue" + } + } + }; + + NSDictionary *diff = @{ + @"messageId1": @{ + @"vars": @{ + @"myNumber": @1, + @"myString": @"newValue" + } + } + }; + + NSDictionary *expected = diff; + NSDictionary *result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([result isEqualToDictionary:expected]); +} + +- (void)testMergeValuesIncludeDefaults { + NSDictionary *vars = @{ + @"messageId1": @{ + @"vars": @{ + @"myNumber": @0, + @"myString": @"defaultValue" + } + } + }; + + NSDictionary *diff = @{ + @"messageId1": @{ + @"vars": @{ + @"myString": @"newValue" + } + } + }; + + NSDictionary *expected = @{ + @"messageId1": @{ + @"vars": @{ + @"myNumber": @0, + @"myString": @"newValue" + } + } + }; + + NSDictionary *result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([result isEqualToDictionary:expected]); +} + +- (void)testMergeDictionaries { + NSDictionary *vars = @{ + @"abc": @"qwe", + @"nested2": @{ + @"a": @"a", + @"c": [NSNull null], + @"d": @4444 + } + }; + + NSDictionary *diff = @{ + @"abc": @"rty", + @"nested2": @{ + @"a": @"a", + @"c": @"value", + @"d": @555 + } + }; + + NSDictionary *expected = @{ + @"abc": @"rty", + @"nested2": @{ + @"a": @"a", + @"c": @"value", + @"d": @555 + } + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([expected isEqualToDictionary:result]); +} + +- (void)testMergeDictionariesWithNull { + NSDictionary *vars = @{ + @"a": [NSNull null], + @"b": [NSNull null] + }; + + NSDictionary *diff = @{ + @"a": @"text", + @"c": [NSNull null] + }; + + NSDictionary *expected = @{ + @"a": @"text", + @"b": [NSNull null], + @"c": [NSNull null] + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([expected isEqualToDictionary:result]); +} + +- (void)testMergeDictionariesIncludeDefaults { + NSDictionary *vars = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123 + }, + @"nested2": @{ + @"a": @"a", + @"b": @[@1, @2, @3, @4], + @"c": [NSNull null], + @"d": @4444 + } + }; + + NSDictionary *diff = @{ + @"nested": @{ + @"abc": @"abc", + @"qwerty": @"qwerty" + }, + @"nested2": @{ + @"a": @"b", + @"d": @111, + @"e": @999 + } + }; + + NSDictionary *expected = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"abc", + @"1": @123, + @"qwerty": @"qwerty" + }, + @"nested2": @{ + @"a": @"b", + @"b": @[@1, @2, @3, @4], + @"c": [NSNull null], + @"d": @111, + @"e": @999 + } + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([expected isEqualToDictionary:result]); +} + +- (void)testMergeDictionariesIncludeDiffs { + NSDictionary *vars = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123 + } + }; + + NSDictionary *diff = @{ + @"nested": @{ + @"qwerty": @"qwerty", + @"nested2": @{ + @"a": @"b" + } + } + }; + + NSDictionary *expected = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123, + @"qwerty": @"qwerty", + @"nested2": @{ + @"a": @"b" + } + } + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([expected isEqualToDictionary:result]); +} + +- (void)testMergeWithEmpty { + NSDictionary *vars = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123 + } + }; + + NSDictionary *diff = @{}; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([vars isEqualToDictionary:result]); +} + +- (void)testMergeEmpty { + NSDictionary *vars = @{}; + + NSDictionary *diff = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123 + } + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([diff isEqualToDictionary:result]); +} + +- (void)testMergeNull { + NSNull *vars = [NSNull null]; + + NSDictionary *diff = @{ + @"abc": @"qwe", + @"nested": @{ + @"abc": @"qwe", + @"1": @123 + } + }; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([diff isEqualToDictionary:result]); +} + +- (void)testMergeWithNull { + NSDictionary *vars = @{}; + + NSNull *diff = [NSNull null]; + + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([[NSNull null] isEqual:result]); +} + +- (void)testMergeDifferentTypes { + NSDictionary *vars = @{ + @"k1": @20, + @"k2": @"hi", + @"k3": @YES, + @"k4": @4.3 + }; + NSDictionary *diff = @{ + @"k1": @21, + @"k3": @NO, + @"k4": @-4.8 + }; + NSDictionary *expected = @{ + @"k1": @21, + @"k2": @"hi", + @"k3": @NO, + @"k4": @-4.8 + }; + NSDictionary *result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([result isEqualToDictionary:expected]); +} + +- (void)testMergeNestedDictionaries { + NSDictionary *vars = @{ + @"k2": @{ + @"m1": @(1), + @"m2": @"hello", + @"m3": @(NO) + }, + @"k3": @{ + @"m1": @(1), + @"m2": @"hello", + @"m3": @(NO) + }, + @"k4": @{ + @"m1": @(1), + @"m2": @"hello", + @"m3": @(NO) + }, + @"k5": @(4.3) + }; + + NSDictionary *diffs = @{ + @"k2": @{ + @"m1": @(1), + @"m2": @"hello", + @"m3": @(NO) + }, + @"k3": @{ + @"m1": @(2), + @"m2": @"bye", + @"m3": @(YES) + }, + @"k4": @{ + @"m1": @(2), + @"m3": @(YES), + @"m4": @"new key" + } + }; + + NSDictionary *expected = @{ + @"k2": @{ + @"m1": @(1), + @"m2": @"hello", + @"m3": @(NO) + }, + @"k3": @{ + @"m1": @(2), + @"m2": @"bye", + @"m3": @(YES) + }, + @"k4": @{ + @"m1": @(2), + @"m2": @"hello", + @"m3": @(YES), + @"m4": @"new key" + }, + @"k5": @(4.3) + }; + + NSDictionary *result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diffs]; + XCTAssertEqualObjects(result, expected); +} + +- (void)testMergeArr { + NSArray *vars = @[@1, @2, @3, @4]; + NSArray *diff = @[@1, @2, @3, @6]; + + // ContentMerger does not support merging arrays + id result = (NSArray *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertNil(result); +} + +- (void)testMergeDictionariesArr { + NSDictionary *vars = @{ + @"arr": @[@1, @2, @3, @4], + }; + + NSDictionary *diff = @{ + @"arr": @[@1, @2, @3, @5], + }; + + // ContentMerger does not support merging arrays, expect vars dictionary + id result = (NSDictionary *)[ContentMerger mergeWithVars:vars diff:diff]; + XCTAssertTrue([vars isEqualToDictionary:result]); +} + +@end diff --git a/CleverTapWatchOS/CleverTapWatchOS.h b/CleverTapWatchOS/CleverTapWatchOS.h index 0c49405d..e6a6d277 100644 --- a/CleverTapWatchOS/CleverTapWatchOS.h +++ b/CleverTapWatchOS/CleverTapWatchOS.h @@ -5,8 +5,8 @@ @interface CleverTapWatchOS : NSObject -- (instancetype)initWithSession:(WCSession* _Nonnull)session; +- (instancetype _Nonnull)initWithSession:(WCSession* _Nonnull)session; -- (void)recordEvent:(NSString *)event withProps:(NSDictionary *)props; +- (void)recordEvent:(NSString *_Nonnull)event withProps:(NSDictionary *_Nonnull)props; @end diff --git a/CleverTapWatchOS/CleverTapWatchOS.m b/CleverTapWatchOS/CleverTapWatchOS.m index 1a5c8178..e122724d 100644 --- a/CleverTapWatchOS/CleverTapWatchOS.m +++ b/CleverTapWatchOS/CleverTapWatchOS.m @@ -9,7 +9,7 @@ @interface CleverTapWatchOS () @implementation CleverTapWatchOS -- (instancetype)initWithSession:(WCSession* _Nonnull)session { +- (instancetype _Nonnull)initWithSession:(WCSession* _Nonnull)session { if (self = [super init]) { self.session = session; } @@ -20,13 +20,12 @@ - (void)sendMessage:(NSString *)type withcontent:(NSDictionary *)content{ if (![self.session isReachable]) { return; } - NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; - message = [content mutableCopy]; + NSMutableDictionary *message = [content mutableCopy]; message[@"clevertap_type"] = type; [self.session sendMessage:message replyHandler:nil errorHandler:nil]; } -- (void)recordEvent:(NSString *)event withProps:(NSDictionary *)props { +- (void)recordEvent:(NSString *_Nonnull)event withProps:(NSDictionary *_Nonnull)props { NSMutableDictionary *content = [[NSMutableDictionary alloc] init]; content[@"event"] = event; content[@"props"] = props; diff --git a/Package.swift b/Package.swift index 1749c53f..851ac0d7 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,8 @@ let package = Package( .headerSearchPath("Inbox/config"), .headerSearchPath("Inbox/controllers"), .headerSearchPath("Inbox/models"), - .headerSearchPath("Inbox/views") + .headerSearchPath("Inbox/views"), + .headerSearchPath("ProductExperiences/") ], linkerSettings: [ .linkedFramework("AVFoundation"), diff --git a/README.md b/README.md index fe514d6e..1c560a99 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ CleverTap Geofence SDK provides Geofencing capabilities to CleverTap iOS SDK. To CleverTap iOS SDK supports Push Primer for push notification runtime permission, refer [Push Primer doc](/docs/PushPrimer.md) for more details. +## Remote Config Variables + +CleverTap iOS SDK supports creating remote config variables, refer [Remote Config Variables](/docs/Variables.md) for more details and usage examples. + ## 𝌡 Example Usage * A [demo application](/ObjCStarter) showing the integration of our SDK in Objective-C language. * A [demo application](/SwiftStarter) showing the integration of our SDK in Swift language. diff --git a/docs/Variables.md b/docs/Variables.md new file mode 100644 index 00000000..36b554bc --- /dev/null +++ b/docs/Variables.md @@ -0,0 +1,363 @@ +# Overview +You can define variables using the CleverTap iOS SDK. When you define a variable in your code, you can sync them to the CleverTap Dashboard via the provided SDK methods. + +# Supported Variable Types + +Currently, CleverTap SDK supports the following variable types: + +- String +- boolean +- Dictionary +- int +- float +- double +- short +- long +- Number + +# Define Variables + +Variables can be defined using a shared or custom CleverTap instance. The Variable is defined using the `defineVar` method, which returns an instance of a `CTVar` variable. You must provide the name and default value of the variable. + +```swift +// Swift + +// Primitive types +let var_string = CleverTap.sharedInstance()?.defineVar(name: "var_string", string: "hello, world") +let var_int = CleverTap.sharedInstance()?.defineVar(name: "var_int", integer: 10) +let var_bool = CleverTap.sharedInstance()?.defineVar(name: "var_bool", boolean: true) +let var_float = CleverTap.sharedInstance()?.defineVar(name: "var_float", float: 6.0) +let var_double = CleverTap.sharedInstance()?.defineVar(name: "var_double", double: 60.999) +let var_short = CleverTap.sharedInstance()?.defineVar(name: "var_short", short: 1) +let var_number = CleverTap.sharedInstance()?.defineVar(name: "var_number", number: NSNumber(value: 32)) +let var_long = CleverTap.sharedInstance()?.defineVar(name: "var_long", long: 64) +// Dictionary +let var_dict = CleverTap.sharedInstance()?.defineVar(name: "var_dict", dictionary: [ + "nested_string": "hello, nested", + "nested_double": 10.5 + ]) + +let var_dict_nested = CleverTap.sharedInstance()?.defineVar(name: "var_dict_complex", dictionary: [ + "nested_int": 1, + "nested_string": "hello, world", + "nested_map": [ + "nested_map_int": 15, + "nested_map_string": "hello, nested map", + ] + ]) + + + +``` +```objectivec +// Objective-C + +#import + +// Primitive types + CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withString:@"hello, world"]; + CTVar *var_int = [[CleverTap sharedInstance] defineVar:@"var_int" withInt:10]; + CTVar *var_bool = [[CleverTap sharedInstance] defineVar:@"var_bool" withBool:YES]; + CTVar *var_float = [[CleverTap sharedInstance] defineVar:@"var_float" withFloat:6.0]; + CTVar *var_double = [[CleverTap sharedInstance] defineVar:@"var_double" withDouble:60.999]; + CTVar *var_short = [[CleverTap sharedInstance] defineVar:@"var_short" withShort:1]; + CTVar *var_number = [[CleverTap sharedInstance] defineVar:@"var_number" withNumber:[[NSNumber alloc] initWithInt:32]]; + CTVar *var_long = [[CleverTap sharedInstance] defineVar:@"var_long" withLong:64]; + // Dictionary + CTVar *var_dict = [[CleverTap sharedInstance] defineVar:@"var_dict" withDictionary:@{ + @"nested_string": @"hello, nested", + @"nested_double": @10.5 + }]; + CTVar *var_dict_nested = [[CleverTap sharedInstance] defineVar:@"var_dict_complex" withDictionary:@{ + @"nested_int": @1, + @"nested_string": @"hello, world", + @"nested_map": @{ + @"nested_map_int": @15, + @"nested_map_string": @"hello, nested map", + } + }]; + +``` + + + +# Setup Callbacks + +CleverTap iOS SDK provides several callbacks for the developer to receive feedback from the SDK. You can use them as per your requirement, using all of them is not mandatory. They are as follows: + +- Status of fetch variables request +- `onVariablesChanged` +- `onceVariablesChanged` +- `onValueChanged` +- Variables Delegate + +## Status of Variables Fetch Request + +This method provides a boolean flag to ensure that the variables are successfully fetched from the server. + +```swift +// Swift + +CleverTap.sharedInstance()?.fetchVariables({ success in + print(success) +}) +``` +```objectivec +// Objective-C + +#import + +[[CleverTap sharedInstance] fetchVariables:^(BOOL success) { + + }]; +``` + + + +## `onVariablesChanged` + +This callback is invoked when variables are initialized with values fetched from the server. It is called each time new values are fetched. + +```swift +// Swift + +let var_string = CleverTap.sharedInstance()?.defineVar(name: "myString", string: "hello,world") +CleverTap.sharedInstance()?.onVariablesChanged { + print("CleverTap.onVariablesChanged: \(var_string?.value ?? "")") +} + + +``` +```objectivec +// Objective-C + +#import + +CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withString:@"hello, world"]; + [[CleverTap sharedInstance] onVariablesChanged:^{ + NSLog(@"CleverTap.onVariablesChanged: %@", [var_string value]); + }]; +``` + + + +## `onceVariablesChanged` + +This callback is invoked when variables are initialized with values fetched from the server. It is called only once. + +```swift +// Swift + +let var_string = CleverTap.sharedInstance()?.defineVar(name: "myString", string: "hello,world") +CleverTap.sharedInstance()?.onceVariablesChanged { + print("CleverTap.onceVariablesChanged: \(var_string?.value ?? "")") +} + +``` +```objectivec +// Objective-C + +#import + +CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withString:@"hello, world"]; + + + [[CleverTap sharedInstance] onceVariablesChanged:^{ + // Executed only once + NSLog(@"CleverTap.onceVariablesChanged: %@", [var_string value]); + }]; + +``` + + + +## `onValueChanged` + +This callback is invoked when the value of the variable changes. + +```swift +// Swift + +let var_string = CleverTap.sharedInstance()?.defineVar(name: "myString", string: "hello,world") +var_string?.onValueChanged { + print("var_string.onValueChanged: \(var_string?.value ?? "")") +} + +``` +```objectivec +// Objective-C + +#import + +CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withString:@"hello, world"]; + [var_string onValueChanged:^{ + NSLog(@"var_string.onValueChanged: %@", [var_string value]); + }]; +``` + + + +## Variables Delegate + +The `VarDelegate` method is implemented to be invoked when the variable value is changed. + +```swift +// Swift + +@objc class VarDelegateImpl: NSObject, VarDelegate { + func valueDidChange(_ variable: CleverTapSDK.Var) { + print("CleverTap \(String(describing: variable.name)):valueDidChange to: \(variable.value!)") + } +} + +var_string?.setDelegate(self) +``` +```objectivec +// Objective-C + +#import + +@interface CTVarDelegateImpl : NSObject +@end + + +@implementation CTVarDelegateImpl +- (void)valueDidChange:(CTVar *)variable { +// valueDidChange +} +@end + +CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; +[var_string setDelegate:del]; +``` + + + +# Sync Defined Variables + +After defining your variables in the code, you must send/sync variables to the server. To do so, the app must be in DEBUG mode and mark a particular CleverTap user profile as a test profile from the CleverTap dashboard. [Learn how to mark a profile as **Test Profile**](https://developer.clevertap.com/docs/concepts-user-profiles#mark-a-user-profile-as-a-test-profile) + +After marking the profile as a test profile, you must sync the app variables in DEBUG mode: + +```swift +// Swift + +// 1. Define CleverTap variables +// … +// 2. Add variables/values changed callbacks +// … + +// 3. Sync CleverTap Variables from DEBUG mode/builds +CleverTap.sharedInstance()?.syncVariables(); +``` +```objectivec +// Objective-C + +// 1. Define CleverTap variables +// … +// 2. Add variables/values changed callbacks +// … + +// 3. Sync CleverTap Variables from DEBUG mode/builds +[[CleverTap sharedInstance] syncVariables]; +``` + + + +> 📘 Key Points to Remember +> +> - In a scenario where there is already a draft created by another user profile in the dashboard, the sync call will fail to avoid overriding important changes made by someone else. In this case, Publish or Dismiss the existing draft before you proceed with syncing variables again. However, you can override a draft you created via the sync method previously to optimize the integration experience. +> - You can receive the following console logs from the CleverTap SDK: +> - Variables synced successfully. +> - Unauthorized access from a non-test profile. Please mark this profile as a test profile from the CleverTap dashboard. + +# Fetch Variables During a Session + +You can fetch the updated values for your CleverTap variables from the server during a session. If variables have changed, the appropriate callbacks will be fired. The provided callback provides a boolean flag that indicates if the fetch call was successful. The callback is fired regardless of whether the variables have changed or not. + +```swift +// Swift + +CleverTap.sharedInstance()?.fetchVariables({ success in + print(success) +}) +``` +```objectivec +// Objective-C + +[[CleverTap sharedInstance] fetchVariables:^(BOOL success) { + +}]; +``` + + + +# Use Fetched Variables Values + +This process involves the following two major steps: + +1. Fetch variable values. +2. Access variable values. + +## Fetch Variable Values + +Variables are updated automatically when server values are received. If you want to receive feedback when a specific variable is updated, use the individual callback: + +```swift +// Swift + +variable?.onValueChanged { + print("variable.onValueChanged: \(variable?.value ?? "")") +} +``` +```objectivec +// Objective-C + +[variable onValueChanged:^{ + NSLog(@"variable.onValueChanged: %@", [variable value]); +}]; +``` + + + +## Access Variable Values + +You can access these fetched values in the following three ways: + +### From `Var` instance + +You can use several methods on the `Var` instance as shown in the following code: + +```swift +// Swift + +variable?.defaultValue // returns default value +variable?.value // returns current value +variable?.numberValue // returns value as NSNumber if applicable +variable?.stringValue // returns value as String +``` +```objectivec +// Objective-C + +variable.defaultValue; // returns default value +variable.value; // returns current value +variable.numberValue; // returns value as NSNumber if applicable +variable.stringValue; // returns value as String +``` + + + +### Using `CleverTap` Instance method + +You can use the `CleverTap` instance method to get the current value of a variable. If the variable is nonexistent, the method returns `null`: + +```swift +// Swift + +CleverTap.sharedInstance()?.getVariableValue("variable name") +``` +```objectivec +// Objective-C + +[[CleverTap sharedInstance]getVariableValue:@"variable name"]; +```