diff --git a/CHANGELOG.md b/CHANGELOG.md index 899b73cf..3e75f575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log All notable changes to this project will be documented in this file. +### [Version 7.0.0](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/7.0.0) (August 07, 2024) + +#### Added +- Adds support for Custom Code Templates. Please refer to the [Custom Code Templates doc](/docs/CustomCodeTemplates.md) to read more on how to integrate this in your app. +- Adds support for File Type Variables in Remote Config. Please refer to the [Remote Config Variables doc](/docs/Variables.md) to read more on how to integrate this in your app. +- Adds support for triggering in-app notifications on User Attribute Change. +- Adds the CleverTap SDK version in the JS interface for HTML in-app notifications. + +#### Fixed +- Fix HTML view controller `CTInAppHTMLViewController` presented before scene became active. +- Use keyWindow supported orientations for `CTInAppDisplayViewController`. + ### [Version 6.2.1](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/6.2.1) (April 12, 2024) #### Fixed diff --git a/CleverTap-iOS-SDK.podspec b/CleverTap-iOS-SDK.podspec index 19fab6cd..3cf8a182 100644 --- a/CleverTap-iOS-SDK.podspec +++ b/CleverTap-iOS-SDK.podspec @@ -15,9 +15,9 @@ s.ios.resource_bundle = {'CleverTapSDK' => ['CleverTapSDK/**/*.{png,xib,ht s.ios.deployment_target = '9.0' s.ios.source_files = 'CleverTapSDK/**/*.{h,m}' s.ios.exclude_files = 'CleverTapSDK/include/**/*.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', 'CleverTapSDK/LeanplumCT.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', 'CleverTapSDK/LeanplumCT.h', 'CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplatePresenter.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplateProducer.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h' s.tvos.deployment_target = '9.0' -s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}', 'CleverTapSDK/ProductExperiences/*.{h,m}', 'CleverTapSDK/Swizzling/*.{h,m}', 'CleverTapSDK/Session/*.{h,m}' -s.tvos.exclude_files = 'CleverTapSDK/include/**/*.h', 'CleverTapSDK/CleverTapJSInterface.{h,m}', 'CleverTapSDK/CTInAppNotification.{h,m}', 'CleverTapSDK/CTPushPrimerManager.{h,m}', 'CleverTapSDK/InApps/*.{h,m}', 'CleverTapSDK/InApps/**/*.{h,m}', 'CleverTapSDK/CTInAppFCManager.{h,m}', 'CleverTapSDK/CTInAppDisplayViewController.{h,m}' +s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/FileDownload/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}', 'CleverTapSDK/ProductExperiences/*.{h,m}', 'CleverTapSDK/Swizzling/*.{h,m}', 'CleverTapSDK/Session/*.{h,m}' +s.tvos.exclude_files = 'CleverTapSDK/include/**/*.h', 'CleverTapSDK/CleverTapJSInterface.{h,m}', 'CleverTapSDK/CTInAppNotification.{h,m}', 'CleverTapSDK/CTNotificationButton.{h,m}', 'CleverTapSDK/CTNotificationAction.{h,m}', 'CleverTapSDK/CTPushPrimerManager.{h,m}', 'CleverTapSDK/InApps/*.{h,m}', 'CleverTapSDK/InApps/**/*.{h,m}', 'CleverTapSDK/CTInAppFCManager.{h,m}', 'CleverTapSDK/CTInAppDisplayViewController.{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', 'CleverTapSDK/CleverTap+CTVar.h', 'CleverTapSDK/ProductExperiences/CTVar.h' end diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 583bc56f..75193683 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -84,9 +84,7 @@ 071EB513217F6427008F0FAB /* CTInAppNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 071EB4C4217F6427008F0FAB /* CTInAppNotification.m */; }; 071EB514217F6427008F0FAB /* CTCoverImageViewController~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 071EB4C5217F6427008F0FAB /* CTCoverImageViewController~ipad.xib */; }; 071EB515217F6427008F0FAB /* CTInterstitialViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 071EB4C6217F6427008F0FAB /* CTInterstitialViewController.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 071EB521217F6764008F0FAB /* CTNotificationButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 071EB4AA217F6427008F0FAB /* CTNotificationButton.h */; settings = {ATTRIBUTES = (Private, ); }; }; 071EB525217F6C8F008F0FAB /* CTUIUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 071EB4C2217F6427008F0FAB /* CTUIUtils.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 071EB527217F6CD1008F0FAB /* CTNotificationButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 071EB481217F6427008F0FAB /* CTNotificationButton.m */; }; 07238E3B221D638D0012DFAE /* CTInterstitialViewController~iphoneland.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07238E3A221D638D0012DFAE /* CTInterstitialViewController~iphoneland.xib */; }; 07238E3D221E79F80012DFAE /* CTInboxSimpleMessageCell~land.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07238E3C221E79F80012DFAE /* CTInboxSimpleMessageCell~land.xib */; }; 07238E3F221E7A100012DFAE /* CTCarouselMessageCell~land.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07238E3E221E7A100012DFAE /* CTCarouselMessageCell~land.xib */; }; @@ -152,6 +150,8 @@ 07D8C08B21DDEC54006F5A1B /* CTCarouselImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = 07D8C08A21DDEC54006F5A1B /* CTCarouselImageView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 07FD65A2223BC26300A845B7 /* CTCoverViewController~iphoneland.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07FD65A1223BC26300A845B7 /* CTCoverViewController~iphoneland.xib */; }; 07FD65A4223BCB8200A845B7 /* CTCoverViewController~ipadland.xib in Resources */ = {isa = PBXBuildFile; fileRef = 07FD65A3223BCB8200A845B7 /* CTCoverViewController~ipadland.xib */; }; + 0B5564562C25946C00B87284 /* CTUserInfoMigratorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */; }; + 0B995A4A2C36AEDC00AF6006 /* CTLocalDataStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */; }; 1F1C18806B7F29B3374F2448 /* libPods-shared-CleverTapSDKTestsApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E303560B5EE1D154C1E3D9EF /* libPods-shared-CleverTapSDKTestsApp.a */; }; 32394C1F29FA251E00956058 /* CTEventBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32394C1E29FA251E00956058 /* CTEventBuilderTest.m */; }; 32394C2129FA264B00956058 /* CTPreferencesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32394C2029FA264B00956058 /* CTPreferencesTest.m */; }; @@ -165,9 +165,13 @@ 4808030E292EB4FB00C06E2F /* CleverTap+PushPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.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 */; }; - 48BEA4F62AFB868B00690424 /* CTInAppImagePrefetchManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */; }; - 48BEA4F82AFB86A300690424 /* CTInAppImagePrefetchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */; }; - 48C31A822B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */; }; + 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */; }; + 48A2C4B92BD67DDC006C61BC /* sampleTXTStub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */; }; + 48A2C4BA2BD67DDC006C61BC /* samplePDFStub.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */; }; + 48C0FD6F2BCD522100E01EA9 /* CTFileDownloadManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */; }; + 48F9FD092C208F7100617770 /* CTInAppStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427D72ABCE5EB0098866F /* CTInAppStore.m */; }; + 48F9FD1D2C3D30BF00617770 /* CTFileDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */; }; + 48F9FD1E2C3D30BF00617770 /* CTFileDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD192C3D30B600617770 /* CTFileDownloader.m */; }; 4987C665251B5E79003E6BE8 /* CTImageInAppViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */; }; 4987C666251B5E79003E6BE8 /* CTImageInAppViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */; }; 4987C668251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987C667251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h */; }; @@ -269,6 +273,10 @@ 4EA64A2E296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; 4EA64A2F296C1190001D9B22 /* CTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA64A2B296C1190001D9B22 /* CTRequest.m */; }; 4EAF05022A495DD5009D9D61 /* CleverTapInstanceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E1F154E27691CA0009387AE /* CleverTapInstanceTests.m */; }; + 4EB3638B2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */; }; + 4EB3638C2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */; }; + 4EB3638D2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4EB3638E2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4EB4C8BE2AAD91AC00B7F045 /* CTTriggerEvaluator.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EB4C8BC2AAD91AC00B7F045 /* CTTriggerEvaluator.h */; }; 4EB4C8BF2AAD91AC00B7F045 /* CTTriggerEvaluator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4C8BD2AAD91AC00B7F045 /* CTTriggerEvaluator.m */; }; 4ECD88312ADC8A05003885CE /* CTSessionManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ECD88302ADC8A05003885CE /* CTSessionManagerTests.m */; }; @@ -313,7 +321,6 @@ 6A4427D02AB9D8C30098866F /* CTInAppFCManager+Legacy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A4427CE2AB9D8C30098866F /* CTInAppFCManager+Legacy.h */; }; 6A4427D12AB9D8C30098866F /* CTInAppFCManager+Legacy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427CF2AB9D8C30098866F /* CTInAppFCManager+Legacy.m */; }; 6A4427D82ABCE5EB0098866F /* CTInAppStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A4427D62ABCE5EB0098866F /* CTInAppStore.h */; }; - 6A4427D92ABCE5EB0098866F /* CTInAppStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427D72ABCE5EB0098866F /* CTInAppStore.m */; }; 6A59D20D2A334B8500531F9D /* NSDictionaryExtensionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A59D20C2A334B8500531F9D /* NSDictionaryExtensionsTest.m */; }; 6A59D20F2A3351A800531F9D /* LeanplumCTTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A59D20E2A3351A800531F9D /* LeanplumCTTest.m */; }; 6A6591662AC70D07005FDE57 /* CTAttachToBatchHeaderDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A6591642AC70D07005FDE57 /* CTAttachToBatchHeaderDelegate.h */; }; @@ -330,16 +337,64 @@ 6AA1357C2A2E467800EFF2C1 /* NSDictionary+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 6AA135782A2E467800EFF2C1 /* NSDictionary+Extensions.m */; }; 6B0063BB2B18EC9E0063BF79 /* image_interstitial.html in Resources */ = {isa = PBXBuildFile; fileRef = 6B0063BA2B18EC9E0063BF79 /* image_interstitial.html */; }; 6B0063BC2B18EC9E0063BF79 /* image_interstitial.html in Resources */ = {isa = PBXBuildFile; fileRef = 6B0063BA2B18EC9E0063BF79 /* image_interstitial.html */; }; + 6B32A09E2B9901AA009ADC57 /* CTCustomTemplateBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B32A09C2B9901AA009ADC57 /* CTCustomTemplateBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6B32A09F2B9901AA009ADC57 /* CTCustomTemplateBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A09D2B9901AA009ADC57 /* CTCustomTemplateBuilder.m */; }; + 6B32A0A12B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B32A0A02B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h */; }; + 6B32A0A32B99EA9D009ADC57 /* CTCustomTemplateBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0A22B99EA9D009ADC57 /* CTCustomTemplateBuilderTest.m */; }; + 6B32A0A52B9A0F17009ADC57 /* CTCustomTemplateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0A42B9A0F17009ADC57 /* CTCustomTemplateTest.m */; }; + 6B32A0AD2B9DBE31009ADC57 /* CTTemplatePresenterMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0AC2B9DBE31009ADC57 /* CTTemplatePresenterMock.m */; }; + 6B32A0B02B9DC374009ADC57 /* CTTemplateArgumentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0AF2B9DC374009ADC57 /* CTTemplateArgumentTest.m */; }; + 6B32A0B42B9F2E8F009ADC57 /* CTTestTemplateProducer.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */; }; 6B4A0F912B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */; }; 6B535FB62AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */; }; 6B535FB72AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */; }; 6B535FB82AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; 6B535FB92AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; + 6B9157B92C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */; }; + 6B9157BB2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */; }; 6B9DEE9F2B4D8A500097EF40 /* clevertap-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */; }; + 6B9E95AC2C27164B0002D557 /* CTFileDownloadTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */; }; + 6B9E95B12C2864EC0002D557 /* CTFileDownloaderMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */; }; + 6B9E95B52C29C2F40002D557 /* NSFileManagerMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */; }; 6BA3B2DB2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; 6BA3B2DC2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; 6BA3B2E12B05411C004E834B /* InAppHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BA3B2E02B05411C004E834B /* InAppHelper.m */; }; 6BA3B2E82B07E207004E834B /* CTTriggersMatcher+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BA3B2E72B07E207004E834B /* CTTriggersMatcher+Tests.m */; }; + 6BAFFE9C2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BAFFE9B2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.m */; }; + 6BAFFEA82C45243B00654CAF /* CTFileDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 48F9FD1A2C3D30B600617770 /* CTFileDownloader.h */; }; + 6BAFFEA92C45243C00654CAF /* CTFileDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 48F9FD1A2C3D30B600617770 /* CTFileDownloader.h */; }; + 6BAFFEAA2C45244100654CAF /* CTFileDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD192C3D30B600617770 /* CTFileDownloader.m */; }; + 6BAFFEAB2C45244400654CAF /* CTFileDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 48F9FD172C3D30B600617770 /* CTFileDownloadManager.h */; }; + 6BAFFEAC2C45244500654CAF /* CTFileDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 48F9FD172C3D30B600617770 /* CTFileDownloadManager.h */; }; + 6BAFFEAD2C45244800654CAF /* CTFileDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */; }; + 6BB727122B8E458D009CE7D0 /* CTTemplateProducer.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727112B8E458D009CE7D0 /* CTTemplateProducer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB727152B8E463C009CE7D0 /* CTCustomTemplate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727132B8E463C009CE7D0 /* CTCustomTemplate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB727162B8E463C009CE7D0 /* CTCustomTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB727142B8E463C009CE7D0 /* CTCustomTemplate.m */; }; + 6BB727192B8E469B009CE7D0 /* CTInAppTemplateBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727172B8E469B009CE7D0 /* CTInAppTemplateBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB7271A2B8E469B009CE7D0 /* CTInAppTemplateBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB727182B8E469B009CE7D0 /* CTInAppTemplateBuilder.m */; }; + 6BB7271D2B8E46AB009CE7D0 /* CTAppFunctionBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB7271B2B8E46AB009CE7D0 /* CTAppFunctionBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB7271E2B8E46AB009CE7D0 /* CTAppFunctionBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB7271C2B8E46AB009CE7D0 /* CTAppFunctionBuilder.m */; }; + 6BB727212B8E55CD009CE7D0 /* CTTemplateContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB7271F2B8E55CD009CE7D0 /* CTTemplateContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB727222B8E55CD009CE7D0 /* CTTemplateContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB727202B8E55CD009CE7D0 /* CTTemplateContext.m */; }; + 6BB727242B8E55DE009CE7D0 /* CTTemplatePresenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727232B8E55DE009CE7D0 /* CTTemplatePresenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB727262B8E5839009CE7D0 /* CTTemplateContext-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727252B8E5839009CE7D0 /* CTTemplateContext-Internal.h */; }; + 6BB7272B2B8E5C66009CE7D0 /* CTTemplateArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727292B8E5C66009CE7D0 /* CTTemplateArgument.h */; }; + 6BB7272C2B8E5C66009CE7D0 /* CTTemplateArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB7272A2B8E5C66009CE7D0 /* CTTemplateArgument.m */; }; + 6BB7272E2B8F3D79009CE7D0 /* CTCustomTemplate-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB7272D2B8F3D79009CE7D0 /* CTCustomTemplate-Internal.h */; }; + 6BB727332B8F787D009CE7D0 /* CTCustomTemplatesManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB727312B8F787D009CE7D0 /* CTCustomTemplatesManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6BB727342B8F787D009CE7D0 /* CTCustomTemplatesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB727322B8F787D009CE7D0 /* CTCustomTemplatesManager.m */; }; + 6BB727362B8F8567009CE7D0 /* CTCustomTemplatesManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB727352B8F8567009CE7D0 /* CTCustomTemplatesManagerTest.m */; }; + 6BB778C72BECEC2700A41628 /* CTCustomTemplateInAppData.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778C52BECEC2700A41628 /* CTCustomTemplateInAppData.h */; }; + 6BB778C82BECEC2700A41628 /* CTCustomTemplateInAppData.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778C62BECEC2700A41628 /* CTCustomTemplateInAppData.m */; }; + 6BB778CB2BED21CE00A41628 /* CTNotificationAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778C92BED21CE00A41628 /* CTNotificationAction.h */; }; + 6BB778CC2BED21CE00A41628 /* CTNotificationAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778CA2BED21CE00A41628 /* CTNotificationAction.m */; }; + 6BB778CE2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778CD2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m */; }; + 6BB778D02BEE4C3400A41628 /* CTNotificationActionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778CF2BEE4C3400A41628 /* CTNotificationActionTest.m */; }; + 6BB778D22BF267B600A41628 /* CTTemplateContextTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778D12BF267B600A41628 /* CTTemplateContextTest.m */; }; + 6BB778D62BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6BB778D72BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6BB778D92BFD277400A41628 /* CTCustomTemplatesManager-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */; }; + 6BBF05CE2C58E3FB0047E3D9 /* NSURLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */; }; 6BD334EA2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */; }; 6BD334EB2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */; }; 6BD334EC2AF2A41F0099E33E /* CTBatchSentDelegateHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BD334E92AF2A41F0099E33E /* CTBatchSentDelegateHelper.m */; }; @@ -696,6 +751,8 @@ 07D8C08A21DDEC54006F5A1B /* CTCarouselImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTCarouselImageView.h; sourceTree = ""; }; 07FD65A1223BC26300A845B7 /* CTCoverViewController~iphoneland.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = "CTCoverViewController~iphoneland.xib"; sourceTree = ""; }; 07FD65A3223BCB8200A845B7 /* CTCoverViewController~ipadland.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = "CTCoverViewController~ipadland.xib"; sourceTree = ""; }; + 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTUserInfoMigratorTest.m; sourceTree = ""; }; + 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTLocalDataStoreTests.m; sourceTree = ""; }; 0CA46771B6F202E37DAC9F70 /* Pods-shared-CleverTapSDKTestsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTestsApp.debug.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTestsApp/Pods-shared-CleverTapSDKTestsApp.debug.xcconfig"; sourceTree = ""; }; 129AEC403AFA828F591B756E /* Pods-shared-CleverTapSDKTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-CleverTapSDKTests.release.xcconfig"; path = "Target Support Files/Pods-shared-CleverTapSDKTests/Pods-shared-CleverTapSDKTests.release.xcconfig"; sourceTree = ""; }; 32394C1E29FA251E00956058 /* CTEventBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventBuilderTest.m; sourceTree = ""; }; @@ -711,9 +768,14 @@ 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CleverTap+PushPermission.h"; sourceTree = ""; }; 4808030F292EB50D00C06E2F /* CTLocalInApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTLocalInApp.h; sourceTree = ""; }; 48080310292EB50D00C06E2F /* CTLocalInApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTLocalInApp.m; sourceTree = ""; }; - 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppImagePrefetchManager.h; sourceTree = ""; }; - 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppImagePrefetchManager.m; sourceTree = ""; }; - 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppImagePrefetchManagerTest.m; sourceTree = ""; }; + 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderTests.m; sourceTree = ""; }; + 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sampleTXTStub.txt; sourceTree = ""; }; + 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = samplePDFStub.pdf; sourceTree = ""; }; + 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadManagerTests.m; sourceTree = ""; }; + 48F9FD172C3D30B600617770 /* CTFileDownloadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTFileDownloadManager.h; sourceTree = ""; }; + 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadManager.m; sourceTree = ""; }; + 48F9FD192C3D30B600617770 /* CTFileDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloader.m; sourceTree = ""; }; + 48F9FD1A2C3D30B600617770 /* CTFileDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTFileDownloader.h; sourceTree = ""; }; 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewController.h; sourceTree = ""; }; 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTImageInAppViewController.m; sourceTree = ""; }; 4987C667251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewControllerPrivate.h; sourceTree = ""; }; @@ -768,6 +830,8 @@ 4EA64A25296C115E001D9B22 /* CTRequestFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTRequestFactory.m; sourceTree = ""; }; 4EA64A2A296C1190001D9B22 /* CTRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTRequest.h; sourceTree = ""; }; 4EA64A2B296C1190001D9B22 /* CTRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTRequest.m; sourceTree = ""; }; + 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTUserInfoMigrator.m; sourceTree = ""; }; + 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTUserInfoMigrator.h; sourceTree = ""; }; 4EB4C8BC2AAD91AC00B7F045 /* CTTriggerEvaluator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTriggerEvaluator.h; sourceTree = ""; }; 4EB4C8BD2AAD91AC00B7F045 /* CTTriggerEvaluator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTriggerEvaluator.m; sourceTree = ""; }; 4EC2D084278AAD8000F4DE54 /* IdentityManagementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IdentityManagementTests.m; sourceTree = ""; }; @@ -827,11 +891,32 @@ 6AF6C7FF2A1BDA6A001E38A8 /* LeanplumCT.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LeanplumCT.h; sourceTree = ""; }; 6AF6C8002A1BDA6A001E38A8 /* LeanplumCT.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LeanplumCT.m; sourceTree = ""; }; 6B0063BA2B18EC9E0063BF79 /* image_interstitial.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = image_interstitial.html; sourceTree = ""; }; + 6B32A09C2B9901AA009ADC57 /* CTCustomTemplateBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTCustomTemplateBuilder.h; sourceTree = ""; }; + 6B32A09D2B9901AA009ADC57 /* CTCustomTemplateBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplateBuilder.m; sourceTree = ""; }; + 6B32A0A02B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplateBuilder-Internal.h"; sourceTree = ""; }; + 6B32A0A22B99EA9D009ADC57 /* CTCustomTemplateBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplateBuilderTest.m; sourceTree = ""; }; + 6B32A0A42B9A0F17009ADC57 /* CTCustomTemplateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplateTest.m; sourceTree = ""; }; + 6B32A0AB2B9DBE31009ADC57 /* CTTemplatePresenterMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTemplatePresenterMock.h; sourceTree = ""; }; + 6B32A0AC2B9DBE31009ADC57 /* CTTemplatePresenterMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplatePresenterMock.m; sourceTree = ""; }; + 6B32A0AF2B9DC374009ADC57 /* CTTemplateArgumentTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplateArgumentTest.m; sourceTree = ""; }; + 6B32A0B12B9F2A75009ADC57 /* CTCustomTemplatesManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplatesManager+Tests.h"; sourceTree = ""; }; + 6B32A0B22B9F2E8F009ADC57 /* CTTestTemplateProducer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTestTemplateProducer.h; sourceTree = ""; }; + 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTestTemplateProducer.m; sourceTree = ""; }; 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppTriggerManagerTest.m; sourceTree = ""; }; 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTMultiDelegateManager.h; sourceTree = ""; }; 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTMultiDelegateManager.m; sourceTree = ""; }; + 6B9157B72C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppNotificationDisplayDelegateMock.h; sourceTree = ""; }; + 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppNotificationDisplayDelegateMock.m; sourceTree = ""; }; + 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplateInAppData-Internal.h"; sourceTree = ""; }; 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "clevertap-logo.png"; sourceTree = ""; }; - 6B9DEEA02B4DF1B70097EF40 /* CTInAppImagePrefetchManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTInAppImagePrefetchManager+Tests.h"; sourceTree = ""; }; + 6B9E95AA2C27164B0002D557 /* CTFileDownloadTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloadTestHelper.h; sourceTree = ""; }; + 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadTestHelper.m; sourceTree = ""; }; + 6B9E95AE2C2864AF0002D557 /* CTFileDownloader+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTFileDownloader+Tests.h"; sourceTree = ""; }; + 6B9E95AF2C2864EC0002D557 /* CTFileDownloaderMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloaderMock.h; sourceTree = ""; }; + 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderMock.m; sourceTree = ""; }; + 6B9E95B22C2868470002D557 /* CTFileDownloadManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTFileDownloadManager+Tests.h"; sourceTree = ""; }; + 6B9E95B32C29C2F30002D557 /* NSFileManagerMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSFileManagerMock.h; sourceTree = ""; }; + 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSFileManagerMock.m; sourceTree = ""; }; 6BA3B2DA2B03E926004E834B /* CTQueueType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTQueueType.h; sourceTree = ""; }; 6BA3B2DF2B05411C004E834B /* InAppHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InAppHelper.h; sourceTree = ""; }; 6BA3B2E02B05411C004E834B /* InAppHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppHelper.m; sourceTree = ""; }; @@ -841,6 +926,36 @@ 6BA3B2E52B07E1D0004E834B /* CTImpressionManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTImpressionManager+Tests.h"; sourceTree = ""; }; 6BA3B2E62B07E207004E834B /* CTTriggersMatcher+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTTriggersMatcher+Tests.h"; sourceTree = ""; }; 6BA3B2E72B07E207004E834B /* CTTriggersMatcher+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CTTriggersMatcher+Tests.m"; sourceTree = ""; }; + 6BAFFE9A2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTFileDownloaderCustomTemplatesMock.h; sourceTree = ""; }; + 6BAFFE9B2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderCustomTemplatesMock.m; sourceTree = ""; }; + 6BB727112B8E458D009CE7D0 /* CTTemplateProducer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTemplateProducer.h; sourceTree = ""; }; + 6BB727132B8E463C009CE7D0 /* CTCustomTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTCustomTemplate.h; sourceTree = ""; }; + 6BB727142B8E463C009CE7D0 /* CTCustomTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplate.m; sourceTree = ""; }; + 6BB727172B8E469B009CE7D0 /* CTInAppTemplateBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppTemplateBuilder.h; sourceTree = ""; }; + 6BB727182B8E469B009CE7D0 /* CTInAppTemplateBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppTemplateBuilder.m; sourceTree = ""; }; + 6BB7271B2B8E46AB009CE7D0 /* CTAppFunctionBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTAppFunctionBuilder.h; sourceTree = ""; }; + 6BB7271C2B8E46AB009CE7D0 /* CTAppFunctionBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTAppFunctionBuilder.m; sourceTree = ""; }; + 6BB7271F2B8E55CD009CE7D0 /* CTTemplateContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTemplateContext.h; sourceTree = ""; }; + 6BB727202B8E55CD009CE7D0 /* CTTemplateContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplateContext.m; sourceTree = ""; }; + 6BB727232B8E55DE009CE7D0 /* CTTemplatePresenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTemplatePresenter.h; sourceTree = ""; }; + 6BB727252B8E5839009CE7D0 /* CTTemplateContext-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTTemplateContext-Internal.h"; sourceTree = ""; }; + 6BB727292B8E5C66009CE7D0 /* CTTemplateArgument.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTemplateArgument.h; sourceTree = ""; }; + 6BB7272A2B8E5C66009CE7D0 /* CTTemplateArgument.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplateArgument.m; sourceTree = ""; }; + 6BB7272D2B8F3D79009CE7D0 /* CTCustomTemplate-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplate-Internal.h"; sourceTree = ""; }; + 6BB727312B8F787D009CE7D0 /* CTCustomTemplatesManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTCustomTemplatesManager.h; sourceTree = ""; }; + 6BB727322B8F787D009CE7D0 /* CTCustomTemplatesManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplatesManager.m; sourceTree = ""; }; + 6BB727352B8F8567009CE7D0 /* CTCustomTemplatesManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplatesManagerTest.m; sourceTree = ""; }; + 6BB778C52BECEC2700A41628 /* CTCustomTemplateInAppData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTCustomTemplateInAppData.h; sourceTree = ""; }; + 6BB778C62BECEC2700A41628 /* CTCustomTemplateInAppData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplateInAppData.m; sourceTree = ""; }; + 6BB778C92BED21CE00A41628 /* CTNotificationAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTNotificationAction.h; sourceTree = ""; }; + 6BB778CA2BED21CE00A41628 /* CTNotificationAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTNotificationAction.m; sourceTree = ""; }; + 6BB778CD2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTCustomTemplateInAppDataTest.m; sourceTree = ""; }; + 6BB778CF2BEE4C3400A41628 /* CTNotificationActionTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTNotificationActionTest.m; sourceTree = ""; }; + 6BB778D12BF267B600A41628 /* CTTemplateContextTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTemplateContextTest.m; sourceTree = ""; }; + 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppNotificationDisplayDelegate.h; sourceTree = ""; }; + 6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplatesManager-Internal.h"; sourceTree = ""; }; + 6BBF05CC2C58E3FB0047E3D9 /* NSURLSessionMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionMock.h; sourceTree = ""; }; + 6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionMock.m; sourceTree = ""; }; 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTBatchSentDelegateHelper.h; sourceTree = ""; }; 6BD334E92AF2A41F0099E33E /* CTBatchSentDelegateHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTBatchSentDelegateHelper.m; sourceTree = ""; }; 6BD334EF2AF545C70099E33E /* CTInAppStoreTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppStoreTest.m; sourceTree = ""; }; @@ -1032,6 +1147,7 @@ 071EB476217F6427008F0FAB /* InApps */ = { isa = PBXGroup; children = ( + 6BB727102B8E455B009CE7D0 /* CustomTemplates */, 6A3EBD2F2AA0701D00CE97D4 /* Matchers */, 4808030F292EB50D00C06E2F /* CTLocalInApp.h */, 48080310292EB50D00C06E2F /* CTLocalInApp.m */, @@ -1080,8 +1196,6 @@ 6BF5A5902ACC854800CDED20 /* CTInAppDisplayManager.m */, 6BF5A59D2AD4303C00CDED20 /* CleverTap+InAppsResponseHandler.m */, 6BF5A5A02AD4313900CDED20 /* CleverTap+InAppsResponseHandler.h */, - 48BEA4F52AFB868B00690424 /* CTInAppImagePrefetchManager.h */, - 48BEA4F72AFB86A300690424 /* CTInAppImagePrefetchManager.m */, ); path = InApps; sourceTree = ""; @@ -1257,6 +1371,8 @@ 4E1F1560277090D6009387AE /* Stub Responses */ = { isa = PBXGroup; children = ( + 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */, + 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */, 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */, 4E1F156727709848009387AE /* app_inbox.json */, 4E1F1561277090D6009387AE /* inapp_alert.json */, @@ -1327,6 +1443,7 @@ 6A4427C32AA6513C0098866F /* InApps */ = { isa = PBXGroup; children = ( + 6B32A0AA2B9B0B6F009ADC57 /* CustomTemplates */, 6A4427C42AA6515A0098866F /* CTTriggersMatcherTest.m */, 4EFC642A2AB44CF900F01414 /* CTLimitsMatcherTest.m */, 6A4427CC2AB8C3B10098866F /* CTInAppEvaluationManagerTest.m */, @@ -1335,7 +1452,6 @@ 6BEEC2CF2AF1A3A900BD4EC5 /* CTClockMock.h */, 6BEEC2D02AF1A3A900BD4EC5 /* CTClockMock.m */, 6BD334EF2AF545C70099E33E /* CTInAppStoreTest.m */, - 48C31A812B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m */, 6BA3B2DF2B05411C004E834B /* InAppHelper.h */, 6BA3B2E02B05411C004E834B /* InAppHelper.m */, 6BA3B2E22B07E06C004E834B /* CTInAppStore+Tests.h */, @@ -1344,8 +1460,8 @@ 6BA3B2E52B07E1D0004E834B /* CTImpressionManager+Tests.h */, 6BA3B2E62B07E207004E834B /* CTTriggersMatcher+Tests.h */, 6BA3B2E72B07E207004E834B /* CTTriggersMatcher+Tests.m */, - 6B9DEEA02B4DF1B70097EF40 /* CTInAppImagePrefetchManager+Tests.h */, 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */, + 6BB778CF2BEE4C3400A41628 /* CTNotificationActionTest.m */, ); path = InApps; sourceTree = ""; @@ -1366,9 +1482,92 @@ path = ProductExperiences; sourceTree = ""; }; + 6B32A0AA2B9B0B6F009ADC57 /* CustomTemplates */ = { + isa = PBXGroup; + children = ( + 6BB727352B8F8567009CE7D0 /* CTCustomTemplatesManagerTest.m */, + 6B32A0A22B99EA9D009ADC57 /* CTCustomTemplateBuilderTest.m */, + 6B32A0A42B9A0F17009ADC57 /* CTCustomTemplateTest.m */, + 6B32A0AB2B9DBE31009ADC57 /* CTTemplatePresenterMock.h */, + 6B32A0AC2B9DBE31009ADC57 /* CTTemplatePresenterMock.m */, + 6B32A0AF2B9DC374009ADC57 /* CTTemplateArgumentTest.m */, + 6B32A0B12B9F2A75009ADC57 /* CTCustomTemplatesManager+Tests.h */, + 6B32A0B22B9F2E8F009ADC57 /* CTTestTemplateProducer.h */, + 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */, + 6BB778CD2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m */, + 6BB778D12BF267B600A41628 /* CTTemplateContextTest.m */, + 6B9157B72C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.h */, + 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */, + 6BAFFE9A2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.h */, + 6BAFFE9B2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.m */, + ); + path = CustomTemplates; + sourceTree = ""; + }; + 6B9E95AD2C285F2F0002D557 /* FileDownload */ = { + isa = PBXGroup; + children = ( + 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */, + 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */, + 6B9E95AA2C27164B0002D557 /* CTFileDownloadTestHelper.h */, + 6B9E95AB2C27164B0002D557 /* CTFileDownloadTestHelper.m */, + 6B9E95AE2C2864AF0002D557 /* CTFileDownloader+Tests.h */, + 6B9E95AF2C2864EC0002D557 /* CTFileDownloaderMock.h */, + 6B9E95B02C2864EC0002D557 /* CTFileDownloaderMock.m */, + 6B9E95B22C2868470002D557 /* CTFileDownloadManager+Tests.h */, + 6B9E95B32C29C2F30002D557 /* NSFileManagerMock.h */, + 6B9E95B42C29C2F30002D557 /* NSFileManagerMock.m */, + 6BBF05CC2C58E3FB0047E3D9 /* NSURLSessionMock.h */, + 6BBF05CD2C58E3FB0047E3D9 /* NSURLSessionMock.m */, + ); + path = FileDownload; + sourceTree = ""; + }; + 6B9E95B62C2AE6740002D557 /* FileDownload */ = { + isa = PBXGroup; + children = ( + 48F9FD1A2C3D30B600617770 /* CTFileDownloader.h */, + 48F9FD192C3D30B600617770 /* CTFileDownloader.m */, + 48F9FD172C3D30B600617770 /* CTFileDownloadManager.h */, + 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */, + ); + path = FileDownload; + sourceTree = ""; + }; + 6BB727102B8E455B009CE7D0 /* CustomTemplates */ = { + isa = PBXGroup; + children = ( + 6BB727112B8E458D009CE7D0 /* CTTemplateProducer.h */, + 6BB727132B8E463C009CE7D0 /* CTCustomTemplate.h */, + 6BB727142B8E463C009CE7D0 /* CTCustomTemplate.m */, + 6BB727172B8E469B009CE7D0 /* CTInAppTemplateBuilder.h */, + 6BB727182B8E469B009CE7D0 /* CTInAppTemplateBuilder.m */, + 6BB7271B2B8E46AB009CE7D0 /* CTAppFunctionBuilder.h */, + 6BB7271C2B8E46AB009CE7D0 /* CTAppFunctionBuilder.m */, + 6BB7271F2B8E55CD009CE7D0 /* CTTemplateContext.h */, + 6BB727202B8E55CD009CE7D0 /* CTTemplateContext.m */, + 6BB727232B8E55DE009CE7D0 /* CTTemplatePresenter.h */, + 6BB727252B8E5839009CE7D0 /* CTTemplateContext-Internal.h */, + 6BB727292B8E5C66009CE7D0 /* CTTemplateArgument.h */, + 6BB7272A2B8E5C66009CE7D0 /* CTTemplateArgument.m */, + 6BB7272D2B8F3D79009CE7D0 /* CTCustomTemplate-Internal.h */, + 6BB727312B8F787D009CE7D0 /* CTCustomTemplatesManager.h */, + 6BB727322B8F787D009CE7D0 /* CTCustomTemplatesManager.m */, + 6B32A09C2B9901AA009ADC57 /* CTCustomTemplateBuilder.h */, + 6B32A09D2B9901AA009ADC57 /* CTCustomTemplateBuilder.m */, + 6B32A0A02B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h */, + 6BB778C52BECEC2700A41628 /* CTCustomTemplateInAppData.h */, + 6BB778C62BECEC2700A41628 /* CTCustomTemplateInAppData.m */, + 6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */, + 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */, + ); + path = CustomTemplates; + sourceTree = ""; + }; D02AC2D9276044F70031C1BE /* CleverTapSDKTests */ = { isa = PBXGroup; children = ( + 6B9E95AD2C285F2F0002D557 /* FileDownload */, 4E2CF1432AC56D8F00441E8B /* CTEncryptionTests.m */, 6A4427C32AA6513C0098866F /* InApps */, 6A7BB8DE29E60BE900651584 /* ProductExperiences */, @@ -1399,6 +1598,8 @@ 6A59D20E2A3351A800531F9D /* LeanplumCTTest.m */, 4ECD88302ADC8A05003885CE /* CTSessionManagerTests.m */, 6BD851C82B45CD1800FA5298 /* CTMultiDelegateManager+Tests.h */, + 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */, + 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */, ); path = CleverTapSDKTests; sourceTree = ""; @@ -1491,6 +1692,7 @@ D0C7BBBF207D82C0001345EF /* CleverTapSDK */ = { isa = PBXGroup; children = ( + 6B9E95B62C2AE6740002D557 /* FileDownload */, 3242D7DA2B1DDA2E00A5E37A /* PrivacyInfo.xcprivacy */, 4803951A2A7ABAD200C4D254 /* CTAES.h */, 480395192A7ABAD200C4D254 /* CTAES.m */, @@ -1581,6 +1783,8 @@ D0CACF8D20B8A44C00A02327 /* CTCertificatePinning.m */, D0CACF9420B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h */, D0CACF9520B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.m */, + 4EB3638A2C087A8200C00AE2 /* CTUserInfoMigrator.h */, + 4EB363892C087A8200C00AE2 /* CTUserInfoMigrator.m */, 071EB4AE217F6427008F0FAB /* CTInAppUtils.h */, 071EB47D217F6427008F0FAB /* CTInAppUtils.m */, 071EB486217F6427008F0FAB /* CTInAppFCManager.h */, @@ -1615,6 +1819,9 @@ 6BD334E82AF2A41F0099E33E /* CTBatchSentDelegateHelper.h */, 6BD334E92AF2A41F0099E33E /* CTBatchSentDelegateHelper.m */, 6BA3B2DA2B03E926004E834B /* CTQueueType.h */, + 6BB778C92BED21CE00A41628 /* CTNotificationAction.h */, + 6BB778CA2BED21CE00A41628 /* CTNotificationAction.m */, + 6BB778D52BFD26DF00A41628 /* CTInAppNotificationDisplayDelegate.h */, ); path = CleverTapSDK; sourceTree = ""; @@ -1667,12 +1874,12 @@ 4E41FD8B294F44200001FBED /* CTVar.h in Headers */, D014B90620E2FB62001E0780 /* CTKnownProfileFields.h in Headers */, D014B8E820E2FA6D001E0780 /* CleverTapBuildInfo.h in Headers */, + 6BAFFEAC2C45244500654CAF /* CTFileDownloadManager.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 */, D014B8EE20E2FAA8001E0780 /* CleverTapUTMDetail.h in Headers */, 6BD334EB2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */, @@ -1683,6 +1890,7 @@ 4E8B816C2AD2B2FD00714BB4 /* CleverTapInternal.h in Headers */, D014B90220E2FB4F001E0780 /* CTEventBuilder.h in Headers */, 6A6591692AC70FFE005FDE57 /* CTBatchSentDelegate.h in Headers */, + 6BB778D72BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */, 07BF465B217F7C41002E166D /* CTInAppDisplayViewController.h in Headers */, 4E25E3D22788889F0008C888 /* CTLoginInfoProvider.h in Headers */, D0BD75AF241769E40006EE55 /* CleverTap+ProductConfig.h in Headers */, @@ -1693,10 +1901,12 @@ D0BD75AC241769710006EE55 /* CleverTap+FeatureFlags.h in Headers */, 4E7929FA29799E8F00B81F3C /* CTDomainFactory.h in Headers */, 4E25E3CC278887A80008C888 /* CTFlexibleIdentityRepo.h in Headers */, + 6BAFFEA92C45243C00654CAF /* CTFileDownloader.h in Headers */, D014B8EB20E2FA94001E0780 /* CleverTapSyncDelegate.h in Headers */, 4E49AE54275D24570074A774 /* CTValidationResultStack.h in Headers */, 07BF465D217F7C88002E166D /* CTInAppDisplayViewControllerPrivate.h in Headers */, 6BD334F42AF7FC660099E33E /* CTTriggerRadius.h in Headers */, + 4EB3638E2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */, 4E838C41299F419900ED0875 /* ContentMerger.h in Headers */, D0BD75A82417694F0006EE55 /* CTFeatureFlagsController.h in Headers */, 4E25E3CD278887A80008C888 /* CTLegacyIdentityRepo.h in Headers */, @@ -1744,6 +1954,7 @@ 57D2E1C82684B1630068E45A /* CleverTap.h in Headers */, D0CACF8B20B8923700A02327 /* CleverTap+SSLPinning.h in Headers */, 4E7704B82679DCEF005222D0 /* CleverTapURLDelegate.h in Headers */, + 6BB7272B2B8E5C66009CE7D0 /* CTTemplateArgument.h in Headers */, 4E25E3C1278887A70008C888 /* CTIdentityRepo.h in Headers */, 071EB4F3217F6427008F0FAB /* CTInAppNotification.h in Headers */, F9356ED42487FE4600B4F507 /* CleverTapPushNotificationDelegate.h in Headers */, @@ -1754,23 +1965,29 @@ 4987C665251B5E79003E6BE8 /* CTImageInAppViewController.h in Headers */, D014B8E220E2F9F9001E0780 /* CleverTapInstanceConfigPrivate.h in Headers */, 071EB4CF217F6427008F0FAB /* CTBaseHeaderFooterViewControllerPrivate.h in Headers */, + 6BB778C72BECEC2700A41628 /* CTCustomTemplateInAppData.h in Headers */, + 6BB727192B8E469B009CE7D0 /* CTInAppTemplateBuilder.h in Headers */, D0BD759D241760C60006EE55 /* CTProductConfigController.h in Headers */, + 6BB7271D2B8E46AB009CE7D0 /* CTAppFunctionBuilder.h in Headers */, 4E41FD8A294F441D0001FBED /* CTVar.h in Headers */, 6A4427B92AA3903C0098866F /* CTTriggerCondition.h in Headers */, 4E49AE53275D24570074A774 /* CTValidationResultStack.h in Headers */, 07B94544219EA34300D4C542 /* CTInboxController.h in Headers */, 4E838C40299F419900ED0875 /* ContentMerger.h in Headers */, 5709005327FD8E1F0011B89F /* CleverTap+SCDomain.h in Headers */, + 6BB727152B8E463C009CE7D0 /* CTCustomTemplate.h in Headers */, D0213D4F207D905800FE5740 /* CleverTapUTMDetail.h in Headers */, 4E5A02DC2A4C5FD100DE242A /* LeanplumCT.h in Headers */, 6A3EBD3A2AA0713100CE97D4 /* CTTriggerValue.h in Headers */, 071EB4D3217F6427008F0FAB /* CTAVPlayerViewController.h in Headers */, + 6BAFFEAB2C45244400654CAF /* CTFileDownloadManager.h in Headers */, 07B9454A219EA34300D4C542 /* CTUserMO+CoreDataProperties.h in Headers */, 6B535FB62AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */, 4EF0D5452AD84BCA0044C48F /* CTSessionManager.h in Headers */, D0213D4C207D905800FE5740 /* CleverTapSyncDelegate.h in Headers */, + 6BB778D92BFD277400A41628 /* CTCustomTemplatesManager-Internal.h in Headers */, 07B94546219EA34300D4C542 /* CTMessageMO+CoreDataProperties.h in Headers */, - 48BEA4F62AFB868B00690424 /* CTInAppImagePrefetchManager.h in Headers */, + 6BB727332B8F787D009CE7D0 /* CTCustomTemplatesManager.h in Headers */, 6BA3B2DB2B03E926004E834B /* CTQueueType.h in Headers */, D01651B22097B42C00660178 /* CTValidator.h in Headers */, 6A4427D02AB9D8C30098866F /* CTInAppFCManager+Legacy.h in Headers */, @@ -1780,6 +1997,7 @@ 4E8B816B2AD2B2FD00714BB4 /* CleverTapInternal.h in Headers */, 07B94541219EA34300D4C542 /* CleverTapInboxViewControllerPrivate.h in Headers */, 071EB4EE217F6427008F0FAB /* CTAlertViewController.h in Headers */, + 4EB3638D2C087A8200C00AE2 /* CTUserInfoMigrator.h in Headers */, 4E838C4629A0C94B00ED0875 /* CleverTap+CTVar.h in Headers */, D0CACF9620B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.h in Headers */, 4E41FD92294F46510001FBED /* CTVar-Internal.h in Headers */, @@ -1788,6 +2006,7 @@ 6BF5A5A12AD4313900CDED20 /* CleverTap+InAppsResponseHandler.h in Headers */, 6BD334F32AF7FC660099E33E /* CTTriggerRadius.h in Headers */, D01651AE2097B38400660178 /* CTEventBuilder.h in Headers */, + 6BB7272E2B8F3D79009CE7D0 /* CTCustomTemplate-Internal.h in Headers */, 072F9E3D21B1368000BC6313 /* CTInboxIconMessageCell.h in Headers */, 4EA64A26296C115E001D9B22 /* CTRequestFactory.h in Headers */, D01651B62097B81400660178 /* CTKnownProfileFields.h in Headers */, @@ -1804,6 +2023,7 @@ 07D8C08B21DDEC54006F5A1B /* CTCarouselImageView.h in Headers */, 071EB515217F6427008F0FAB /* CTInterstitialViewController.h in Headers */, 0701E9622372C1950034AAC2 /* CTDisplayUnitController.h in Headers */, + 6BAFFEA82C45243B00654CAF /* CTFileDownloader.h in Headers */, 4E7929F929799E8F00B81F3C /* CTDomainFactory.h in Headers */, 6A775C3329BE78C7007790E0 /* CTVariables.h in Headers */, 0797132F21A2F09A0011C9A3 /* CTSwipeView.h in Headers */, @@ -1826,6 +2046,7 @@ 6A11D83A2A8FC71F007F5D21 /* CTImpressionManager.h in Headers */, 4987C668251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h in Headers */, 6A4427B52AA38E500098866F /* CTTriggerAdapter.h in Headers */, + 6BB727242B8E55DE009CE7D0 /* CTTemplatePresenter.h in Headers */, D01A0899207ED98300423D6F /* CTPlistInfo.h in Headers */, D032F3A82093EC9700F98D74 /* CTValidationResult.h in Headers */, 0796FB6921AE5B6300FC380D /* CTCarouselImageMessageCell.h in Headers */, @@ -1833,6 +2054,8 @@ 6BEEC2C62AE9B02100BD4EC5 /* CTSystemClock.h in Headers */, D020C928209006AD0073F61E /* CTUriHelper.h in Headers */, D0596EFA208A6A9000A80753 /* CTSwizzle.h in Headers */, + 6B32A09E2B9901AA009ADC57 /* CTCustomTemplateBuilder.h in Headers */, + 6BB727122B8E458D009CE7D0 /* CTTemplateProducer.h in Headers */, 071EB508217F6427008F0FAB /* CTBaseHeaderFooterViewController.h in Headers */, D0A84AD8209135D400191B1F /* CTPreferences.h in Headers */, 0701E95C2372BA250034AAC2 /* CleverTap+DisplayUnit.h in Headers */, @@ -1848,12 +2071,16 @@ 6BD334EA2AF2A41F0099E33E /* CTBatchSentDelegateHelper.h in Headers */, 071EB4F9217F6427008F0FAB /* CTNotificationButton.h in Headers */, 48080311292EB50D00C06E2F /* CTLocalInApp.h in Headers */, + 6BB778CB2BED21CE00A41628 /* CTNotificationAction.h in Headers */, 4803951C2A7ABAD200C4D254 /* CTAES.h in Headers */, D0213D4D207D905800FE5740 /* CleverTapTrackedViewController.h in Headers */, 4E25E3C5278887A70008C888 /* CTFlexibleIdentityRepo.h in Headers */, + 6BB727262B8E5839009CE7D0 /* CTTemplateContext-Internal.h in Headers */, 071EB512217F6427008F0FAB /* CTInAppHTMLViewController.h in Headers */, + 6BB727212B8E55CD009CE7D0 /* CTTemplateContext.h in Headers */, 4E8B816F2AD2BB8A00714BB4 /* CTDispatchQueueManager.h in Headers */, D01A0894207EC2D400423D6F /* CleverTapInstanceConfig.h in Headers */, + 6BB778D62BFD26E000A41628 /* CTInAppNotificationDisplayDelegate.h in Headers */, 4808030E292EB4FB00C06E2F /* CleverTap+PushPermission.h in Headers */, 071EB50C217F6427008F0FAB /* CTCoverImageViewController.h in Headers */, 4E8B81782AD2CB4E00714BB4 /* CTPushPrimerManager.h in Headers */, @@ -1866,7 +2093,9 @@ D0405B4622050C5200D64EC3 /* CTInboxUtils.h in Headers */, 4E25E3C6278887A70008C888 /* CTLegacyIdentityRepo.h in Headers */, 6BF5A5912ACC854800CDED20 /* CTInAppDisplayManager.h in Headers */, + 6B32A0A12B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h in Headers */, 07B94547219EA34300D4C542 /* CTMessageMO.h in Headers */, + 6B9157BB2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h in Headers */, 4E25E3C2278887A70008C888 /* CTIdentityRepoFactory.h in Headers */, 071EB4FF217F6427008F0FAB /* CTHeaderViewController.h in Headers */, 071EB4D6217F6427008F0FAB /* CTInAppFCManager.h in Headers */, @@ -2042,6 +2271,8 @@ 6B9DEE9F2B4D8A500097EF40 /* clevertap-logo.png in Resources */, 4E1F156827709849009387AE /* app_inbox.json in Resources */, 4E1F1562277090D6009387AE /* inapp_alert.json in Resources */, + 48A2C4B92BD67DDC006C61BC /* sampleTXTStub.txt in Resources */, + 48A2C4BA2BD67DDC006C61BC /* samplePDFStub.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2207,6 +2438,7 @@ 4EA64A2F296C1190001D9B22 /* CTRequest.m in Sources */, 49C189A6243B13110003E4D4 /* CleverTapFeatureFlags.m in Sources */, 4E41FD95294F46510001FBED /* CTVar.m in Sources */, + 6BAFFEAD2C45244800654CAF /* CTFileDownloadManager.m in Sources */, 4E6383DA296DE9A8001E83E3 /* CTRequestSender.m in Sources */, D014B8ED20E2FAA2001E0780 /* CleverTapTrackedViewController.m in Sources */, 4E5A02DF2A4C5FD800DE242A /* LeanplumCT.m in Sources */, @@ -2239,6 +2471,7 @@ 6AA1357C2A2E467800EFF2C1 /* NSDictionary+Extensions.m in Sources */, D014B8EF20E2FAAD001E0780 /* CleverTapUTMDetail.m in Sources */, 4E41FD9D294F46510001FBED /* CTVarCache.m in Sources */, + 4EB3638C2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */, 4E4E17852B50007D009E2F1E /* CTAES.m in Sources */, D014B91D20E2FBD6001E0780 /* CTCertificatePinning.m in Sources */, D014B90920E2FB71001E0780 /* CTLocalDataStore.m in Sources */, @@ -2253,7 +2486,7 @@ 6BD334F62AF7FC660099E33E /* CTTriggerRadius.m in Sources */, D014B8FD20E2FB18001E0780 /* CTPreferences.m in Sources */, 4E49AE56275D24570074A774 /* CTValidationResultStack.m in Sources */, - 071EB527217F6CD1008F0FAB /* CTNotificationButton.m in Sources */, + 6BAFFEAA2C45244100654CAF /* CTFileDownloader.m in Sources */, 07BF4663217F838D002E166D /* CTInAppUtils.m in Sources */, 49C18999242CF6730003E4D4 /* CleverTapConfigValue.m in Sources */, ); @@ -2265,39 +2498,57 @@ files = ( 6BA3B2E12B05411C004E834B /* InAppHelper.m in Sources */, 32394C2529FA272600956058 /* CTValidatorTest.m in Sources */, + 6BB778CE2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m in Sources */, 6BA3B2E82B07E207004E834B /* CTTriggersMatcher+Tests.m in Sources */, + 6BBF05CE2C58E3FB0047E3D9 /* NSURLSessionMock.m in Sources */, + 6B32A0A52B9A0F17009ADC57 /* CTCustomTemplateTest.m in Sources */, 4E1F155B276B662C009387AE /* EventDetail.m in Sources */, 6B4A0F912B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m in Sources */, 4EFC642B2AB44CF900F01414 /* CTLimitsMatcherTest.m in Sources */, 4EED219B29AF6368006CEA19 /* CTVarCacheTest.m in Sources */, 6BF5A5A42AD45B4D00CDED20 /* CTInAppFCManagerTest.m in Sources */, + 6B32A0A32B99EA9D009ADC57 /* CTCustomTemplateBuilderTest.m in Sources */, + 6B9E95B52C29C2F40002D557 /* NSFileManagerMock.m in Sources */, 6A7BB8DC29E47CFF00651584 /* CTVarTest.m in Sources */, + 6B32A0AD2B9DBE31009ADC57 /* CTTemplatePresenterMock.m in Sources */, 6A2E0B9129CCCC8600FCEA5F /* ContentMergerTest.m in Sources */, 6A2E0B9529D49D0200FCEA5F /* CTVariables+Tests.m in Sources */, + 6B32A0B42B9F2E8F009ADC57 /* CTTestTemplateProducer.m in Sources */, 6A2E0B9829D49D5100FCEA5F /* CTVarCacheMock.m in Sources */, 4EAF05022A495DD5009D9D61 /* CleverTapInstanceTests.m in Sources */, + 6BB778D22BF267B600A41628 /* CTTemplateContextTest.m in Sources */, 6A2E0B9329D0A5CF00FCEA5F /* CTVariablesTest.m in Sources */, D02AC2DB276044F70031C1BE /* CleverTapSDKTests.m in Sources */, 32394C2129FA264B00956058 /* CTPreferencesTest.m in Sources */, 6BD334F02AF545C80099E33E /* CTInAppStoreTest.m in Sources */, 32394C1F29FA251E00956058 /* CTEventBuilderTest.m in Sources */, + 6BB778D02BEE4C3400A41628 /* CTNotificationActionTest.m in Sources */, D02AC2EB2767F4590031C1BE /* BaseTestCase.m in Sources */, + 6BAFFE9C2C371B4500654CAF /* CTFileDownloaderCustomTemplatesMock.m in Sources */, 6BEEC2CE2AEC49F100BD4EC5 /* CTImpressionManagerTest.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, 4E2BFB9C2AD69BCA00DEB247 /* XCTestCase+XCTestCase_Tests.m in Sources */, + 6B9157B92C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m in Sources */, 6A59D20F2A3351A800531F9D /* LeanplumCTTest.m in Sources */, 32790957299CC099001FE140 /* CTUtilsTest.m in Sources */, + 48C0FD6F2BCD522100E01EA9 /* CTFileDownloadManagerTests.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, 6BEEC2D12AF1A3A900BD4EC5 /* CTClockMock.m in Sources */, 32790959299F4B29001FE140 /* CTDeviceInfoTest.m in Sources */, - 48C31A822B1DC5CF00CA2A90 /* CTInAppImagePrefetchManagerTest.m in Sources */, 4ECD88312ADC8A05003885CE /* CTSessionManagerTests.m in Sources */, + 0B995A4A2C36AEDC00AF6006 /* CTLocalDataStoreTests.m in Sources */, 6A4427C52AA6515A0098866F /* CTTriggersMatcherTest.m in Sources */, + 6B32A0B02B9DC374009ADC57 /* CTTemplateArgumentTest.m in Sources */, + 0B5564562C25946C00B87284 /* CTUserInfoMigratorTest.m in Sources */, 4E2CF1442AC56D8F00441E8B /* CTEncryptionTests.m in Sources */, 32394C2729FA278C00956058 /* CTUriHelperTest.m in Sources */, + 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */, 6A59D20D2A334B8500531F9D /* NSDictionaryExtensionsTest.m in Sources */, + 6BB727362B8F8567009CE7D0 /* CTCustomTemplatesManagerTest.m in Sources */, 4E1F155227692A11009387AE /* CleverTap+Tests.m in Sources */, + 6B9E95AC2C27164B0002D557 /* CTFileDownloadTestHelper.m in Sources */, 6A4427CD2AB8C3B10098866F /* CTInAppEvaluationManagerTest.m in Sources */, + 6B9E95B12C2864EC0002D557 /* CTFileDownloaderMock.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2327,6 +2578,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 48F9FD092C208F7100617770 /* CTInAppStore.m in Sources */, 071EB509217F6427008F0FAB /* CTAlertViewController.m in Sources */, 07B9454B219EA34300D4C542 /* CleverTapInboxMessage.m in Sources */, 4E41FD9C294F46510001FBED /* CTVarCache.m in Sources */, @@ -2336,9 +2588,9 @@ 6A4427C22AA39AC30098866F /* CTLimitsMatcher.m in Sources */, 071EB4F5217F6427008F0FAB /* CTInAppHTMLViewController.m in Sources */, D01651AF2097B38400660178 /* CTEventBuilder.m in Sources */, - 6A4427D92ABCE5EB0098866F /* CTInAppStore.m in Sources */, D020C929209006AD0073F61E /* CTUriHelper.m in Sources */, 07B9454C219EA34300D4C542 /* CTMessageMO.m in Sources */, + 48F9FD1E2C3D30BF00617770 /* CTFileDownloader.m in Sources */, D0D4C9F32414EE6C0029477E /* CleverTapFeatureFlags.m in Sources */, 4E5A02DE2A4C5FD800DE242A /* LeanplumCT.m in Sources */, 6AA1357B2A2E467800EFF2C1 /* NSDictionary+Extensions.m in Sources */, @@ -2347,21 +2599,26 @@ D079742A21FE2F2300773602 /* CTVideoThumbnailGenerator.m in Sources */, 071EB4C9217F6427008F0FAB /* CTHeaderViewController.m in Sources */, 6A3EBD3B2AA0713100CE97D4 /* CTTriggerValue.m in Sources */, + 6BB778CC2BED21CE00A41628 /* CTNotificationAction.m in Sources */, 4E25E3C7278887A70008C888 /* CTIdentityRepoFactory.m in Sources */, + 6BB7271E2B8E46AB009CE7D0 /* CTAppFunctionBuilder.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 */, + 6BB7272C2B8E5C66009CE7D0 /* CTTemplateArgument.m in Sources */, 0701E975237A9A760034AAC2 /* CleverTapDisplayUnitContent.m in Sources */, 4987C666251B5E79003E6BE8 /* CTImageInAppViewController.m in Sources */, D0405B4722050C5200D64EC3 /* CTInboxUtils.m in Sources */, D01651B32097B42C00660178 /* CTValidator.m in Sources */, + 6B32A09F2B9901AA009ADC57 /* CTCustomTemplateBuilder.m in Sources */, 49C189A2243B08A40003E4D4 /* CTFeatureFlagsController.m in Sources */, 6A3EBD2E2AA06EB300CE97D4 /* CTInAppEvaluationManager.m in Sources */, 4E838C42299F419900ED0875 /* ContentMerger.m in Sources */, 071EB513217F6427008F0FAB /* CTInAppNotification.m in Sources */, + 6BB727342B8F787D009CE7D0 /* CTCustomTemplatesManager.m in Sources */, D0CACF9720B8A4F800A02327 /* CTPinnedNSURLSessionDelegate.m in Sources */, D0047B0F2098E2F00019C6FD /* CTProfileBuilder.m in Sources */, D0A6626E20801E7F00B403F3 /* CTDeviceInfo.m in Sources */, @@ -2387,6 +2644,7 @@ 4E8B81792AD2CB4E00714BB4 /* CTPushPrimerManager.m in Sources */, 6A4427B62AA38E500098866F /* CTTriggerAdapter.m in Sources */, 4EA64A2E296C1190001D9B22 /* CTRequest.m in Sources */, + 6BB727222B8E55CD009CE7D0 /* CTTemplateContext.m in Sources */, 071EB4CE217F6427008F0FAB /* CTInAppDisplayViewController.m in Sources */, 6A3EBD372AA0705900CE97D4 /* CTLimitAdapter.m in Sources */, 6BF5A5932ACC854800CDED20 /* CTInAppDisplayManager.m in Sources */, @@ -2405,15 +2663,16 @@ 071EB4D4217F6427008F0FAB /* CTBaseHeaderFooterViewController.m in Sources */, D06F052921E802D400D1B6BD /* CTInboxBaseMessageCell.m in Sources */, 07B9453F219EA34300D4C542 /* CleverTapInboxStyleConfig.m in Sources */, + 6BB7271A2B8E469B009CE7D0 /* CTInAppTemplateBuilder.m in Sources */, 6A775C3529BE78C7007790E0 /* CTVariables.m in Sources */, 4E7929FB29799E8F00B81F3C /* CTDomainFactory.m in Sources */, 0796FB6421AE5B2900FC380D /* CTCarouselMessageCell.m in Sources */, + 6BB727162B8E463C009CE7D0 /* CTCustomTemplate.m in Sources */, 071EB4F6217F6427008F0FAB /* CTUIUtils.m in Sources */, 071EB4FA217F6427008F0FAB /* CTAVPlayerViewController.m in Sources */, 6A11D83B2A8FC71F007F5D21 /* CTImpressionManager.m in Sources */, 071EB506217F6427008F0FAB /* CTInAppFCManager.m in Sources */, 4E25E3C3278887A70008C888 /* CTFlexibleIdentityRepo.m in Sources */, - 48BEA4F82AFB86A300690424 /* CTInAppImagePrefetchManager.m in Sources */, 48080312292EB50D00C06E2F /* CTLocalInApp.m in Sources */, D01651B72097B81400660178 /* CTKnownProfileFields.m in Sources */, 07B94545219EA34300D4C542 /* CTUserMO+CoreDataProperties.m in Sources */, @@ -2430,7 +2689,9 @@ 4E49AE55275D24570074A774 /* CTValidationResultStack.m in Sources */, D01A0895207EC2D400423D6F /* CleverTapInstanceConfig.m in Sources */, 071EB4C8217F6427008F0FAB /* CTDismissButton.m in Sources */, + 6BB778C82BECEC2700A41628 /* CTCustomTemplateInAppData.m in Sources */, 4EF0D5472AD84BCA0044C48F /* CTSessionManager.m in Sources */, + 4EB3638B2C087A8200C00AE2 /* CTUserInfoMigrator.m in Sources */, 4EB4C8BF2AAD91AC00B7F045 /* CTTriggerEvaluator.m in Sources */, 0701E9652372DE9C0034AAC2 /* CleverTapDisplayUnit.m in Sources */, 072F9E3E21B1368000BC6313 /* CTInboxIconMessageCell.m in Sources */, @@ -2438,6 +2699,7 @@ 0797133021A2F09A0011C9A3 /* CTSwipeView.m in Sources */, 4E8B81662AD2ADAE00714BB4 /* CTSwizzleManager.m in Sources */, 071EB4EF217F6427008F0FAB /* CTInterstitialViewController.m in Sources */, + 48F9FD1D2C3D30BF00617770 /* CTFileDownloadManager.m in Sources */, D0CACF9920B8A78600A02327 /* CTConstants.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CleverTapSDK/CTBatchSentDelegate.h b/CleverTapSDK/CTBatchSentDelegate.h index b7445a72..d9e5b96f 100644 --- a/CleverTapSDK/CTBatchSentDelegate.h +++ b/CleverTapSDK/CTBatchSentDelegate.h @@ -7,11 +7,12 @@ // #import +#import "CTQueueType.h" @protocol CTBatchSentDelegate @optional -- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success; +- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType; @optional - (void)onAppLaunchedWithSuccess:(BOOL)success; diff --git a/CleverTapSDK/CTConstants.h b/CleverTapSDK/CTConstants.h index c21fd5b2..a3820ec0 100644 --- a/CleverTapSDK/CTConstants.h +++ b/CleverTapSDK/CTConstants.h @@ -50,6 +50,11 @@ extern NSString *const kSessionId; #define CLTAP_SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) #define CLTAP_APP_LAUNCHED_EVENT @"App Launched" #define CLTAP_CHARGED_EVENT @"Charged" +#define CLTAP_PROFILE @"profile" +#define CLTAP_USER_ATTRIBUTE_CHANGE @"_change" +#define CLTAP_KEY_NEW_VALUE @"newValue" +#define CLTAP_KEY_OLD_VALUE @"oldValue" +#define CLTAP_KEY_PROFILE_ATTR_NAME @"profileAttrName" #define CLTAP_EVENT_NAME @"evtName" #define CLTAP_EVENT_DATA @"evtData" #define CLTAP_CHARGED_EVENT_ITEMS @"Items" @@ -75,6 +80,25 @@ extern NSString *const kSessionId; #define CLTAP_NOTIFICATION_CLICKED_TAG @"wzrk_cts" #define CLTAP_NOTIFICATION_TAG @"W$" #define CLTAP_DATE_FORMAT @"yyyyMMdd" +#define CLTAP_DATE_PREFIX @"$D_" + +// profile commands +static NSString *const kCLTAP_COMMAND_SET = @"$set"; +static NSString *const kCLTAP_COMMAND_ADD = @"$add"; +static NSString *const kCLTAP_COMMAND_REMOVE = @"$remove"; +static NSString *const kCLTAP_COMMAND_INCREMENT = @"$incr"; +static NSString *const kCLTAP_COMMAND_DECREMENT = @"$decr"; +static NSString *const kCLTAP_COMMAND_DELETE = @"$delete"; + +#define CLTAP_MULTIVAL_COMMANDS @[kCLTAP_COMMAND_SET, kCLTAP_COMMAND_ADD, kCLTAP_COMMAND_REMOVE] + +#pragma mark Constants for File Assets +#define CLTAP_FILE_URLS_EXPIRY_DICT @"file_urls_expiry_dict" +#define CLTAP_FILE_ASSETS_LAST_DELETED_TS @"cs_file_assets_last_deleted_timestamp" +#define CLTAP_FILE_EXPIRY_OFFSET (60 * 60 * 24 * 7 * 2) // 2 weeks +#define CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL 25 +#define CLTAP_FILE_MAX_CONCURRENCY_COUNT 10 +#define CLTAP_FILES_DIRECTORY_NAME @"CleverTap_Files" #pragma mark Constants for App fields #define CLTAP_APP_VERSION @"Version" @@ -94,10 +118,10 @@ extern NSString *CT_KIND_FLOAT; extern NSString *CT_KIND_STRING; extern NSString *CT_KIND_BOOLEAN; extern NSString *CT_KIND_DICTIONARY; +extern NSString *CT_KIND_FILE; 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; @@ -106,7 +130,6 @@ extern NSString *CT_PE_BOOL_TYPE; extern NSString *CT_PE_DEFAULT_VALUE; extern NSString *CLTAP_PROFILE_IDENTITY_KEY; -#define CLTAP_DEFINE_VARS_URL @"/defineVars" #pragma mark Constants for In-App Notifications #define CLTAP_INAPP_JSON_RESPONSE_KEY @"inapp_notifs" @@ -124,6 +147,8 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_INAPP_SUPPRESSED_META_KEY @"inapps_suppressed" #define CLTAP_INAPP_SS_EVAL_STORAGE_KEY @"inapps_eval" #define CLTAP_INAPP_SUPPRESSED_STORAGE_KEY @"inapps_suppressed" +#define CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE @"inapps_eval_profile" +#define CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE @"inapps_suppressed_profile" #define CLTAP_PREFS_INAPP_SESSION_MAX_KEY @"imc_max" #define CLTAP_PREFS_INAPP_LAST_DATE_KEY @"ict_date" @@ -144,6 +169,7 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_PROP_WZRK_ID @"wzrk_id" #define CLTAP_PROP_VARIANT @"Variant" #define CLTAP_PROP_WZRK_PIVOT @"wzrk_pivot" +#define CLTAP_PROP_WZRK_CTA @"wzrk_c2a" #define CLTAP_INAPP_ID @"ti" #define CLTAP_INAPP_TTL @"wzrk_ttl" @@ -155,6 +181,10 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_INAPP_TOTAL_LIFETIME_COUNT @"tlc" #define CLTAP_INAPP_EXCLUDE_FROM_CAPS @"efc" #define CLTAP_INAPP_EXCLUDE_GLOBAL_CAPS @"excludeGlobalFCaps" +#define CLTAP_INAPP_MEDIA @"media" +#define CLTAP_INAPP_MEDIA_LANDSCAPE @"mediaLandscape" +#define CLTAP_INAPP_MEDIA_CONTENT_TYPE @"content_type" +#define CLTAP_INAPP_MEDIA_URL @"url" #define CLTAP_TRIGGER_BOOL_STRING_YES @"true" #define CLTAP_TRIGGER_BOOL_STRING_NO @"false" @@ -183,6 +213,13 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_INAPP_HTML_TYPE @"custom-html" +#define CLTAP_INAPP_TYPE @"type" +#define CLTAP_INAPP_TEMPLATE_NAME @"templateName" +#define CLTAP_INAPP_TEMPLATE_ID @"templateId" +#define CLTAP_INAPP_TEMPLATE_DESCRIPTION @"templateDescription" +#define CLTAP_INAPP_VARS @"vars" +#define CLTAP_INAPP_ACTIONS @"actions" + #define CLTAP_INAPP_PREVIEW_TYPE @"wzrk_inapp_type" #define CLTAP_INAPP_IMAGE_INTERSTITIAL_TYPE @"image-interstitial" #define CLTAP_INAPP_IMAGE_INTERSTITIAL_CONFIG @"imageInterstitialConfig" @@ -241,10 +278,9 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #pragma mark Constants for Profile identifier keys #define CLTAP_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email"] // LEGACY KEYS #define CLTAP_ALL_PROFILE_IDENTIFIER_KEYS @[@"Identity", @"Email", @"Phone"] +#define CLTAP_SKIP_KEYS_USER_ATTRIBUTE_EVALUATION @[@"cc", @"tz", @"Carrier"] #pragma mark Constants for Encryption #define CLTAP_ENCRYPTION_LEVEL @"CleverTapEncryptionLevel" #define CLTAP_ENCRYPTION_IV @"__CL3>3Rt#P__1V_" #define CLTAP_ENCRYPTION_PII_DATA (@[@"Identity", @"userEmail", @"userPhone", @"userName"]); - - diff --git a/CleverTapSDK/CTConstants.m b/CleverTapSDK/CTConstants.m index 6813fe58..4a61e130 100644 --- a/CleverTapSDK/CTConstants.m +++ b/CleverTapSDK/CTConstants.m @@ -12,12 +12,11 @@ NSString *CT_KIND_FLOAT = @"float"; NSString *CT_KIND_STRING = @"string"; NSString *CT_KIND_BOOLEAN = @"bool"; +NSString *CT_KIND_FILE = @"file"; 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"; diff --git a/CleverTapSDK/CTInAppDisplayViewController.h b/CleverTapSDK/CTInAppDisplayViewController.h index 978fd603..e86833b2 100644 --- a/CleverTapSDK/CTInAppDisplayViewController.h +++ b/CleverTapSDK/CTInAppDisplayViewController.h @@ -1,29 +1,10 @@ #import #import "CTInAppNotification.h" +#import "CTInAppNotificationDisplayDelegate.h" #if !(TARGET_OS_TV) #import "CleverTapJSInterface.h" #endif -@class CTInAppDisplayViewController; - -@protocol CTInAppNotificationDisplayDelegate -- (void)handleNotificationCTA:(NSURL*)ctaURL buttonCustomExtras:(NSDictionary *)buttonCustomExtras forNotification:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller withExtras:(NSDictionary*)extras; -- (void)notificationDidDismiss:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller; -/** - Called when in-app button is tapped for requesting push permission. - */ -- (void)handleInAppPushPrimer:(CTInAppNotification*)notification - fromViewController:(CTInAppDisplayViewController*)controller - withFallbackToSettings:(BOOL)isFallbackToSettings; - -/** - Called to notify that local in-app push primer is dismissed. - */ -- (void)inAppPushPrimerDidDismissed; -@optional -- (void)notificationDidShow:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller; -@end - @interface CTInAppDisplayViewController : UIViewController @property (nonatomic, weak) id delegate; @@ -31,12 +12,13 @@ - (instancetype)init __unavailable; - (instancetype)initWithNotification:(CTInAppNotification*)notification; -#if !(TARGET_OS_TV) -- (instancetype)initWithNotification:(CTInAppNotification*)notification jsInterface:(CleverTapJSInterface *)jsInterface; -#endif + +- (void)initializeWindowOfClass:(Class)windowClass animated:(BOOL)animated; - (void)show:(BOOL)animated; - (void)hide:(BOOL)animated; - (BOOL)deviceOrientationIsLandscape; +- (void)triggerInAppAction:(CTNotificationAction *)action callToAction:(NSString *)callToAction buttonId:(NSString *)buttonId; + @end diff --git a/CleverTapSDK/CTInAppDisplayViewController.m b/CleverTapSDK/CTInAppDisplayViewController.m index 8eea0028..2c699215 100644 --- a/CleverTapSDK/CTInAppDisplayViewController.m +++ b/CleverTapSDK/CTInAppDisplayViewController.m @@ -38,7 +38,7 @@ - (instancetype)initWithNotification:(CTInAppNotification *)notification { self = [super init]; if (self) { _notification = notification; - if (@available(iOS 13.0, *)) { + if (@available(iOS 13, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sceneDidActivate:) name:UISceneDidActivateNotification object:nil]; @@ -47,18 +47,11 @@ - (instancetype)initWithNotification:(CTInAppNotification *)notification { return self; } -#if !(TARGET_OS_TV) -- (instancetype)initWithNotification:(CTInAppNotification *)notification jsInterface:(CleverTapJSInterface *)jsInterface { - self = [self initWithNotification:notification]; - return self; -} -#endif - // Notification will not be posted if the scene became active before registering the observer. // However, this means that there is already an active scene when the controller is initialized. // In this case, we do not need the notification, since showFromWindow will directly find the window from the already active scene and not wait for it. - (void)sceneDidActivate:(NSNotification *)notification -API_AVAILABLE(ios(13.0)) { +API_AVAILABLE(ios(13.0), tvos(13.0)) { if (!self.window && self.waitingForSceneWindow) { CleverTapLogStaticDebug(@"%@:%@: Scene did activate. Showing from window.", [CTInAppDisplayViewController class], self); self.waitingForSceneWindow = NO; @@ -76,17 +69,55 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id *buttons; @property (nonatomic, copy, readonly) NSDictionary *jsonDescription; -@property (nonatomic, readonly) NSString *error; +@property (nonatomic) NSString *error; @property (nonatomic, copy, readonly) NSDictionary *customExtras; @property (nonatomic, copy, readwrite) NSDictionary *actionExtras; @@ -63,14 +65,19 @@ @property (nonatomic, readonly) BOOL fallBackToNotificationSettings; @property (nonatomic, readonly) BOOL skipSettingsAlert; +@property (nonatomic, readonly) CTCustomTemplateInAppData *customTemplateInAppData; + - (instancetype)init __unavailable; #if !CLEVERTAP_NO_INAPP_SUPPORT -- (instancetype)initWithJSON:(NSDictionary*)json - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager; +- (instancetype)initWithJSON:(NSDictionary*)json; #endif -- (void)prepareWithCompletionHandler: (void (^)(void))completionHandler; - + (NSString * _Nullable)inAppId:(NSDictionary * _Nullable)inApp; +- (void)setPreparedInAppImage:(UIImage * _Nullable)inAppImage + inAppImageData:(NSData * _Nullable)inAppImageData error:(NSString * _Nullable)error; + +- (void)setPreparedInAppImageLandscape:(UIImage * _Nullable)inAppImageLandscape + inAppImageLandscapeData:(NSData * _Nullable)inAppImageLandscapeData error:(NSString * _Nullable)error; + @end diff --git a/CleverTapSDK/CTInAppNotification.m b/CleverTapSDK/CTInAppNotification.m index 46d31566..e9c04e27 100644 --- a/CleverTapSDK/CTInAppNotification.m +++ b/CleverTapSDK/CTInAppNotification.m @@ -12,16 +12,15 @@ @interface CTInAppNotification() { @property (nonatomic, readwrite) NSString *Id; @property (nonatomic, readwrite) NSString *campaignId; -@property (nonatomic, readwrite) NSString *type; @property (nonatomic, readwrite) CTInAppType inAppType; -@property (nonatomic, strong) NSURL *imageURL; -@property (nonatomic, strong) NSURL *imageUrlLandscape; +@property (nonatomic, strong, readwrite) NSURL *imageURL; +@property (nonatomic, strong, readwrite) NSURL *imageUrlLandscape; @property (nonatomic, readwrite, strong) UIImage *inAppImage; @property (nonatomic, readwrite, strong) UIImage *inAppImageLandscape; -@property (nonatomic, readwrite, strong) NSData *image; -@property (nonatomic, readwrite, strong) NSData *imageLandscape; +@property (nonatomic, readwrite, strong) NSData *imageData; +@property (nonatomic, readwrite, strong) NSData *imageLandscapeData; @property (nonatomic, copy, readwrite) NSString *contentType; @property (nonatomic, copy, readwrite) NSString *landscapeContentType; @property (nonatomic, copy, readwrite) NSString *mediaUrl; @@ -64,9 +63,7 @@ @interface CTInAppNotification() { @property (nonatomic, readwrite) BOOL fallBackToNotificationSettings; @property (nonatomic, readwrite) BOOL skipSettingsAlert; -@property (nonatomic, readwrite) NSString *error; - -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, readwrite) CTCustomTemplateInAppData *customTemplateInAppData; @end @@ -77,11 +74,9 @@ @implementation CTInAppNotification: NSObject @synthesize mediaIsAudio=_mediaIsAudio; @synthesize mediaIsVideo=_mediaIsVideo; -- (instancetype)initWithJSON:(NSDictionary *)jsonObject - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager { +- (instancetype)initWithJSON:(NSDictionary *)jsonObject { if (self = [super init]) { @try { - self.imagePrefetchManager = imagePrefetchManager; self.inAppType = CTInAppTypeUnknown; self.jsonDescription = jsonObject; self.campaignId = (NSString*) jsonObject[CLTAP_NOTIFICATION_ID_TAG]; @@ -106,11 +101,12 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject [self legacyConfigureFromJSON:jsonObject]; } else { [self configureFromJSON:jsonObject]; + self.customTemplateInAppData = [CTCustomTemplateInAppData createWithJSON:jsonObject]; } if (self.inAppType == CTInAppTypeUnknown) { self.error = @"Unknown InApp Type"; } - + NSUInteger timeToLive = [jsonObject[CLTAP_INAPP_TTL] longValue]; if (timeToLive) { _timeToLive = timeToLive; @@ -129,10 +125,7 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject } - (void)configureFromJSON: (NSDictionary *)jsonObject { - self.type = (NSString*) jsonObject[@"type"]; - if (self.type) { - self.inAppType = [CTInAppUtils inAppTypeFromString:self.type]; - } + self.inAppType = [CTInAppUtils inAppTypeFromString:jsonObject[@"type"]]; self.backgroundColor = jsonObject[@"bg"]; self.title = (NSString*) jsonObject[@"title"][@"text"]; self.titleColor = (NSString*) jsonObject[@"title"][@"color"]; @@ -142,10 +135,10 @@ - (void)configureFromJSON: (NSDictionary *)jsonObject { self.tablet = [jsonObject[@"tablet"] boolValue]; self.hasPortrait = jsonObject[@"hasPortrait"] ? [jsonObject[@"hasPortrait"] boolValue] : YES; self.hasLandscape = jsonObject[@"hasLandscape"] ? [jsonObject[@"hasLandscape"] boolValue] : NO; - NSDictionary *_media = (NSDictionary*) jsonObject[@"media"]; + NSDictionary *_media = (NSDictionary*) jsonObject[CLTAP_INAPP_MEDIA]; if (_media) { - self.contentType = _media[@"content_type"]; - NSString *_mediaUrl = _media[@"url"]; + self.contentType = _media[CLTAP_INAPP_MEDIA_CONTENT_TYPE]; + NSString *_mediaUrl = _media[CLTAP_INAPP_MEDIA_URL]; if (_mediaUrl && _mediaUrl.length > 0) { if ([self.contentType hasPrefix:@"image"]) { self.imageURL = [NSURL URLWithString:_mediaUrl]; @@ -166,10 +159,10 @@ - (void)configureFromJSON: (NSDictionary *)jsonObject { } } - NSDictionary *_mediaLandscape = (NSDictionary*) jsonObject[@"mediaLandscape"]; + NSDictionary *_mediaLandscape = (NSDictionary*) jsonObject[CLTAP_INAPP_MEDIA_LANDSCAPE]; if (_mediaLandscape) { - self.landscapeContentType = _mediaLandscape[@"content_type"]; - NSString *_mediaUrlLandscape = _mediaLandscape[@"url"]; + self.landscapeContentType = _mediaLandscape[CLTAP_INAPP_MEDIA_CONTENT_TYPE]; + NSString *_mediaUrlLandscape = _mediaLandscape[CLTAP_INAPP_MEDIA_URL]; if (_mediaUrlLandscape && _mediaUrlLandscape.length > 0) { if ([self.landscapeContentType hasPrefix:@"image"]) { self.imageUrlLandscape = [NSURL URLWithString:_mediaUrlLandscape]; @@ -307,58 +300,18 @@ - (BOOL)deviceOrientationIsLandscape { #endif } -- (void)prepareWithCompletionHandler: (void (^)(void))completionHandler { -#if !(TARGET_OS_TV) - if ([NSThread isMainThread]) { - self.error = [NSString stringWithFormat:@"[%@ prepareWithCompletionHandler] should not be called on the main thread", [self class]]; - completionHandler(); - return; - } - - if (self.imageURL) { - UIImage *image = [self loadImageIfPresentInDiskCache:self.imageURL]; - if (image) { - self.inAppImage = image; - self.error = nil; - } else { - NSError *error = nil; - NSData *imageData = [NSData dataWithContentsOfURL:self.imageURL options:NSDataReadingMappedIfSafe error:&error]; - if (error || !imageData) { - self.error = [NSString stringWithFormat:@"unable to load image from URL: %@", self.imageURL]; - } else { - if ([self.contentType isEqualToString:@"image/gif"] ) { - SDAnimatedImage *gif = [SDAnimatedImage imageWithData:imageData]; - if (gif == nil) { - self.error = [NSString stringWithFormat:@"unable to decode gif for URL: %@", self.imageURL]; - } - } - self.image = self.error ? nil : imageData; - } - } - } - if (self.imageUrlLandscape && self.hasLandscape) { - UIImage *image = [self loadImageIfPresentInDiskCache:self.imageUrlLandscape]; - if (image) { - self.inAppImageLandscape = image; - self.error = nil; - } else { - NSError *error = nil; - NSData *imageData = [NSData dataWithContentsOfURL:self.imageUrlLandscape options:NSDataReadingMappedIfSafe error:&error]; - if (error || !imageData) { - self.error = [NSString stringWithFormat:@"unable to load landscape image from URL: %@", self.imageUrlLandscape]; - } else { - if ([self.landscapeContentType isEqualToString:@"image/gif"] ) { - SDAnimatedImage *gif = [SDAnimatedImage imageWithData:imageData]; - if (gif == nil) { - self.error = [NSString stringWithFormat:@"unable to decode landscape gif for URL: %@", self.imageUrlLandscape]; - } - } - self.imageLandscape = self.error ? nil : imageData; - } - } - } -#endif - completionHandler(); +- (void)setPreparedInAppImage:(UIImage *)inAppImage + inAppImageData:(NSData *)inAppImageData error:(NSString *)error { + self.error = error; + self.inAppImage = inAppImage; + self.imageData = inAppImageData; +} + +- (void)setPreparedInAppImageLandscape:(UIImage *)inAppImageLandscape + inAppImageLandscapeData:(NSData *)inAppImageLandscapeData error:(NSString *)error { + self.error = error; + self.inAppImageLandscape = inAppImageLandscape; + self.imageLandscapeData = inAppImageLandscapeData; } - (BOOL)validateLegacyJSON:(NSDictionary *)jsonObject { @@ -432,13 +385,6 @@ - (BOOL)isKeyValidInDictionary:(NSDictionary *)d forKey:(NSString *)key ofClass: return FALSE; } -- (UIImage *)loadImageIfPresentInDiskCache:(NSURL *)imageURL { - NSString *imageURLString = [imageURL absoluteString]; - UIImage *image = [self.imagePrefetchManager loadImageFromDisk:imageURLString]; - if (image) return image; - return nil; -} - + (NSString * _Nullable)inAppId:(NSDictionary * _Nullable)inApp { if (inApp && inApp[CLTAP_INAPP_ID]) { NSString *inAppId = [NSString stringWithFormat:@"%@", inApp[CLTAP_INAPP_ID]]; diff --git a/CleverTapSDK/CTInAppNotificationDisplayDelegate.h b/CleverTapSDK/CTInAppNotificationDisplayDelegate.h new file mode 100644 index 00000000..04c079d4 --- /dev/null +++ b/CleverTapSDK/CTInAppNotificationDisplayDelegate.h @@ -0,0 +1,38 @@ +// +// CTInAppNotificationDisplayDelegate.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 21.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTInAppNotificationDisplayDelegate_h +#define CTInAppNotificationDisplayDelegate_h + +@class CTInAppDisplayViewController; +@class CTInAppNotification; +@class CTNotificationAction; + +@protocol CTInAppNotificationDisplayDelegate + +- (void)notificationDidShow:(CTInAppNotification *)notification; + +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras; + +- (void)notificationDidDismiss:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller; + +/** + Called when in-app button is tapped for requesting push permission. + */ +- (void)handleInAppPushPrimer:(CTInAppNotification *)notification + fromViewController:(CTInAppDisplayViewController *)controller + withFallbackToSettings:(BOOL)isFallbackToSettings; + +/** + Called to notify that local in-app push primer is dismissed. + */ +- (void)inAppPushPrimerDidDismissed; + +@end + +#endif /* Header_h */ diff --git a/CleverTapSDK/CTInAppUtils.h b/CleverTapSDK/CTInAppUtils.h index 790d823f..6c67df35 100644 --- a/CleverTapSDK/CTInAppUtils.h +++ b/CleverTapSDK/CTInAppUtils.h @@ -13,11 +13,24 @@ typedef NS_ENUM(NSUInteger, CTInAppType){ CTInAppTypeInterstitialImage, CTInAppTypeHalfInterstitialImage, CTInAppTypeCoverImage, + CTInAppTypeCustom +}; + +typedef NS_ENUM(NSUInteger, CTInAppActionType){ + CTInAppActionTypeUnknown, + CTInAppActionTypeClose, + CTInAppActionTypeOpenURL, + CTInAppActionTypeKeyValues, + CTInAppActionTypeCustom, + CTInAppActionTypeRequestForPermission }; @interface CTInAppUtils : NSObject -+ (CTInAppType)inAppTypeFromString:(NSString*_Nonnull)type; ++ (CTInAppType)inAppTypeFromString:(NSString *_Nonnull)type; ++ (NSString * _Nonnull)inAppTypeString:(CTInAppType)type; ++ (CTInAppActionType)inAppActionTypeFromString:(NSString *_Nonnull)type; ++ (NSString * _Nonnull)inAppActionTypeString:(CTInAppActionType)type; + (NSBundle *_Nullable)bundle; + (NSString *_Nullable)getXibNameForControllerName:(NSString *_Nonnull)controllerName; diff --git a/CleverTapSDK/CTInAppUtils.m b/CleverTapSDK/CTInAppUtils.m index 82ed9209..8fab2616 100644 --- a/CleverTapSDK/CTInAppUtils.m +++ b/CleverTapSDK/CTInAppUtils.m @@ -6,11 +6,14 @@ #import "CTUIUtils.h" #endif -static NSDictionary *_inAppTypeMap; +static NSDictionary *_inAppTypeMap; +static NSDictionary *_inAppTypeToStringMap; +static NSDictionary *_inAppActionTypeStringToTypeMap; +static NSDictionary *_inAppActionTypeTypeToStringMap; @implementation CTInAppUtils -+ (CTInAppType)inAppTypeFromString:(NSString*)type { ++ (NSDictionary *)inAppTypeStringToTypeMap { if (_inAppTypeMap == nil) { _inAppTypeMap = @{ CLTAP_INAPP_HTML_TYPE: @(CTInAppTypeHTML), @@ -22,17 +25,74 @@ + (CTInAppType)inAppTypeFromString:(NSString*)type { @"alert-template": @(CTInAppTypeAlert), @"interstitial-image": @(CTInAppTypeInterstitialImage), @"half-interstitial-image": @(CTInAppTypeHalfInterstitialImage), - @"cover-image": @(CTInAppTypeCoverImage) + @"cover-image": @(CTInAppTypeCoverImage), + @"custom-code": @(CTInAppTypeCustom) }; } - - NSNumber *_type = type != nil ? _inAppTypeMap[type] : @(CTInAppTypeUnknown); + return _inAppTypeMap; +} + ++ (NSDictionary *)inAppTypeTypeToStringMap { + if (_inAppTypeToStringMap == nil) { + NSDictionary *dict = [self inAppTypeStringToTypeMap]; + NSMutableDictionary *swapped = [NSMutableDictionary new]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + swapped[value] = key; + }]; + _inAppTypeToStringMap = [swapped copy]; + } + return _inAppTypeToStringMap; +} + ++ (CTInAppType)inAppTypeFromString:(NSString*)type { + NSNumber *_type = type != nil ? [self inAppTypeStringToTypeMap][type] : @(CTInAppTypeUnknown); if (_type == nil) { _type = @(CTInAppTypeUnknown); } return [_type integerValue]; } ++ (NSString * _Nonnull)inAppTypeString:(CTInAppType)type { + return self.inAppTypeTypeToStringMap[@(type)]; +} + ++ (NSDictionary *)inAppActionTypeTypeToStringMap { + if (_inAppActionTypeTypeToStringMap == nil) { + NSDictionary *dict = [self inAppActionTypeStringToTypeMap]; + NSMutableDictionary *swapped = [NSMutableDictionary new]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + swapped[value] = key; + }]; + _inAppActionTypeTypeToStringMap = [swapped copy]; + } + return _inAppActionTypeTypeToStringMap; +} + ++ (NSDictionary *)inAppActionTypeStringToTypeMap { + if (_inAppActionTypeStringToTypeMap == nil) { + _inAppActionTypeStringToTypeMap = @{ + @"close": @(CTInAppActionTypeClose), + @"url": @(CTInAppActionTypeOpenURL), + @"kv": @(CTInAppActionTypeKeyValues), + @"custom-code": @(CTInAppActionTypeCustom), + @"rfp": @(CTInAppActionTypeRequestForPermission) + }; + } + return _inAppActionTypeStringToTypeMap; +} + ++ (CTInAppActionType)inAppActionTypeFromString:(NSString* _Nonnull)type { + NSNumber *_type = type != nil ? [self inAppActionTypeStringToTypeMap][type] : @(CTInAppActionTypeUnknown); + if (_type == nil) { + _type = @(CTInAppActionTypeUnknown); + } + return [_type integerValue]; +} + ++ (NSString * _Nonnull)inAppActionTypeString:(CTInAppActionType)type { + return self.inAppActionTypeTypeToStringMap[@(type)]; +} + + (NSBundle *)bundle { #if CLEVERTAP_NO_INAPP_SUPPORT return nil; diff --git a/CleverTapSDK/CTLocalDataStore.h b/CleverTapSDK/CTLocalDataStore.h index 7dce5431..2bbd9c02 100644 --- a/CleverTapSDK/CTLocalDataStore.h +++ b/CleverTapSDK/CTLocalDataStore.h @@ -1,12 +1,14 @@ #import #import "CTDeviceInfo.h" +#import "CTDispatchQueueManager.h" @class CleverTapInstanceConfig; @class CleverTapEventDetail; @interface CTLocalDataStore : NSObject -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo; + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo dispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager; - (void)persistEvent:(NSDictionary *)event; @@ -34,6 +36,8 @@ - (id)getProfileFieldForKey:(NSString *)key; +- (NSDictionary *> *)getUserAttributeChangeProperties:(NSDictionary *)event; + - (void)persistLocalProfileIfRequired; - (NSDictionary*)generateBaseProfile; diff --git a/CleverTapSDK/CTLocalDataStore.m b/CleverTapSDK/CTLocalDataStore.m index 15874d99..ffc2d16f 100644 --- a/CleverTapSDK/CTLocalDataStore.m +++ b/CleverTapSDK/CTLocalDataStore.m @@ -11,6 +11,10 @@ #import "CTPreferences.h" #import "CTUtils.h" #import "CTUIUtils.h" +#import "CTUserInfoMigrator.h" +#import "CTDispatchQueueManager.h" +#import "CTMultiDelegateManager.h" +#import "CTProfileBuilder.h" static const void *const kProfileBackgroundQueueKey = &kProfileBackgroundQueueKey; static const double kProfilePersistenceIntervalSeconds = 30.f; @@ -29,15 +33,17 @@ @interface CTLocalDataStore() { @property (nonatomic, strong) CleverTapInstanceConfig *config; @property (nonatomic, strong) CTDeviceInfo *deviceInfo; @property (nonatomic, strong) NSArray *piiKeys; +@property (nonatomic, strong) CTDispatchQueueManager *dispatchQueueManager; @end @implementation CTLocalDataStore -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo { +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo dispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager { if (self = [super init]) { _config = config; _deviceInfo = deviceInfo; + self.dispatchQueueManager = dispatchQueueManager; localProfileUpdateExpiryStore = [NSMutableDictionary new]; _backgroundQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.clevertap.profileBackgroundQueue:%@", _config.accountId] UTF8String], DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_backgroundQueue, kProfileBackgroundQueueKey, (__bridge void *)self, NULL); @@ -45,6 +51,9 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:( _piiKeys = CLTAP_ENCRYPTION_PII_DATA; [self runOnBackgroundQueue:^{ @synchronized (self->localProfileForSession) { + // migrate to new persisted ct-accid-guid-userprofile + [CTUserInfoMigrator migrateUserInfoFileForAccountID:self->_config.accountId deviceID:self->_deviceInfo.deviceId]; + self->localProfileForSession = [self _inflateLocalProfile]; for (NSString* key in [profileValues allKeys]) { [self setProfileFieldWithKey:key andValue:profileValues[key]]; @@ -88,9 +97,12 @@ - (BOOL)inBackgroundQueue { - (void)changeUser { localProfileUpdateExpiryStore = [NSMutableDictionary new]; - localProfileForSession = [NSMutableDictionary dictionary]; - // this will remove the old profile from the file system - [self _persistLocalProfileAsyncWithCompletion:nil]; + localProfileForSession = [NSMutableDictionary new]; + [self runOnBackgroundQueue:^{ + @synchronized (self->localProfileForSession) { + self->localProfileForSession = [self _inflateLocalProfile]; + } + }]; [self clearStoredEvents]; } @@ -511,6 +523,73 @@ - (id)getProfileFieldForKey:(NSString *)key { } } +- (NSDictionary *> *)getUserAttributeChangeProperties:(NSDictionary *)event { + NSMutableDictionary *> *userAttributesChangeProperties = [NSMutableDictionary dictionary]; + NSMutableDictionary *fieldsToPersistLocally = [NSMutableDictionary dictionary]; + NSDictionary *profile = event[CLTAP_PROFILE]; + if (!profile) { + return @{}; + } + for (NSString *key in profile) { + if ([CLTAP_SKIP_KEYS_USER_ATTRIBUTE_EVALUATION containsObject: key]) { + continue; + } + id oldValue = [self getProfileFieldForKey:key]; + id newValue = profile[key]; + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + if ([newValue isKindOfClass:[NSDictionary class]]) { + NSDictionary *obj = (NSDictionary *)newValue; + NSString *commandIdentifier = [[obj allKeys] firstObject]; + id value = [obj objectForKey:commandIdentifier]; + if ([commandIdentifier isEqualToString:kCLTAP_COMMAND_INCREMENT] || + [commandIdentifier isEqualToString:kCLTAP_COMMAND_DECREMENT]) { + newValue = [CTProfileBuilder _getUpdatedValue:value forKey:key withCommand:commandIdentifier cachedValue:oldValue]; + } else if ([commandIdentifier isEqualToString:kCLTAP_COMMAND_DELETE]) { + newValue = nil; + [self removeProfileFieldForKey:key]; + } + } else if ([newValue isKindOfClass:[NSString class]]) { + // Remove the date prefix before evaluation and persisting + NSString *newValueStr = (NSString *)newValue; + if ([newValueStr hasPrefix:CLTAP_DATE_PREFIX]) { + newValue = @([[newValueStr substringFromIndex:[CLTAP_DATE_PREFIX length]] longLongValue]); + } + } + if (oldValue != nil && ![oldValue isKindOfClass:[NSArray class]]) { + [properties setObject:oldValue forKey:CLTAP_KEY_OLD_VALUE]; + } + if (newValue != nil && ![newValue isKindOfClass:[NSArray class]]) { + [properties setObject:newValue forKey:CLTAP_KEY_NEW_VALUE]; + } + + // Skip evaluation if both newValue or oldValue are null + if ([properties count] > 0) { + [userAttributesChangeProperties setObject:properties forKey:key]; + } + // Need to persist only if the new profile value is not a null value + if (newValue != nil && newValue != oldValue) { + [fieldsToPersistLocally setObject:newValue forKey:key]; + } + } + [self updateProfileFieldsLocally:fieldsToPersistLocally]; + return userAttributesChangeProperties; +} + +-(void) updateProfileFieldsLocally: (NSMutableDictionary *) fieldsToPersistLocally{ + [self.dispatchQueueManager runSerialAsync:^{ + [CTProfileBuilder build:fieldsToPersistLocally completionHandler:^(NSDictionary *customFields, NSDictionary *systemFields, NSArray*errors) { + if (systemFields) { + CleverTapLogInternal(self.config.logLevel, @"%@: Constructed system profile: %@", self, systemFields); + [self setProfileFields:systemFields]; + } + if (customFields) { + CleverTapLogInternal(self.config.logLevel, @"%@: Constructed custom profile: %@", self, customFields); + [self setProfileFields:customFields]; + } + }]; + }]; +} + - (void)setProfileFields:(NSDictionary *)fields { [self setProfileFields:fields fromUpstream:NO]; } @@ -615,14 +694,16 @@ - (id)_getProfileFieldFromSessionCacheWithKey:(NSString *)key { id val = nil; @synchronized (localProfileForSession) { - val = localProfileForSession[key]; + // CACHED VALUES HAVE a "user" PREFIX, SO PREPEND IT BEFORE SEARCHING CACHE + NSString *keyToSearch = [localProfileForSession.allKeys containsObject:key] ? key : [NSString stringWithFormat:@"user%@",key]; + val = localProfileForSession[keyToSearch]; } return val; } - (NSString *)profileFileName { - return [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", self.config.accountId]; + return [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", self.config.accountId, _deviceInfo.deviceId]; } - (NSMutableDictionary *)_inflateLocalProfile { diff --git a/CleverTapSDK/CTMultiDelegateManager.h b/CleverTapSDK/CTMultiDelegateManager.h index 5896688b..aa5e60e0 100644 --- a/CleverTapSDK/CTMultiDelegateManager.h +++ b/CleverTapSDK/CTMultiDelegateManager.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)addBatchSentDelegate:(id)delegate; - (void)removeBatchSentDelegate:(id)delegate; -- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success; +- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType; @end diff --git a/CleverTapSDK/CTMultiDelegateManager.m b/CleverTapSDK/CTMultiDelegateManager.m index c9c21a24..0a2e4f02 100644 --- a/CleverTapSDK/CTMultiDelegateManager.m +++ b/CleverTapSDK/CTMultiDelegateManager.m @@ -76,11 +76,11 @@ - (void)removeBatchSentDelegate:(id)delegate { [self.batchSentDelegates removeObject:delegate]; } -- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success { +- (void)notifyDelegatesBatchDidSend:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType{ NSNumber *isAppLaunched = nil; for (id batchSentDelegate in self.batchSentDelegates) { - if ([batchSentDelegate respondsToSelector:@selector(onBatchSent: withSuccess:)]) { - [batchSentDelegate onBatchSent:batchWithHeader withSuccess:success]; + if ([batchSentDelegate respondsToSelector:@selector(onBatchSent: withSuccess:withQueueType:)]) { + [batchSentDelegate onBatchSent:batchWithHeader withSuccess:success withQueueType:queueType]; } if ([batchSentDelegate respondsToSelector:@selector(onAppLaunchedWithSuccess:)]) { if (isAppLaunched == nil) { diff --git a/CleverTapSDK/CTNotificationAction.h b/CleverTapSDK/CTNotificationAction.h new file mode 100644 index 00000000..128e6f98 --- /dev/null +++ b/CleverTapSDK/CTNotificationAction.h @@ -0,0 +1,33 @@ +// +// CTNotificationAction.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 9.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTCustomTemplateInAppData.h" +#import "CTInAppUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTNotificationAction : NSObject + +@property (nonatomic, readonly) CTInAppActionType type; +@property (nonatomic, copy, readonly) NSURL *actionURL; +@property (nonatomic, strong, readonly) NSDictionary *keyValues; +@property (nonatomic, readonly) BOOL fallbackToSettings; +@property (nonatomic, strong, readonly) CTCustomTemplateInAppData *customTemplateInAppData; + +@property (nonatomic, readonly) NSString *error; + +- (instancetype)init NS_UNAVAILABLE; +#if !CLEVERTAP_NO_INAPP_SUPPORT +- (instancetype)initWithJSON:(NSDictionary *)json; +- (instancetype)initWithOpenURL:(NSURL *)url; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/CTNotificationAction.m b/CleverTapSDK/CTNotificationAction.m new file mode 100644 index 00000000..6eea6643 --- /dev/null +++ b/CleverTapSDK/CTNotificationAction.m @@ -0,0 +1,58 @@ +// +// CTNotificationAction.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 9.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTNotificationAction.h" + +@interface CTNotificationAction() + +@property (nonatomic, readwrite) CTInAppActionType type; +@property (nonatomic, copy, readwrite) NSURL *actionURL; +@property (nonatomic, strong, readwrite) NSDictionary *keyValues; +@property (nonatomic, readwrite) BOOL fallbackToSettings; +@property (nonatomic, strong, readwrite) CTCustomTemplateInAppData *customTemplateInAppData; +@property (nonatomic, readwrite) NSString *error; + +@end + +@implementation CTNotificationAction + +- (nonnull instancetype)initWithJSON:(nonnull NSDictionary *)json { + if (self = [super init]) { + @try { + id kv = json[@"kv"]; + if ([kv isKindOfClass:[NSDictionary class]]) { + self.keyValues = kv; + } + NSString *action = json[@"ios"]; + if (action && action.length > 0) { + @try { + self.actionURL = [NSURL URLWithString:action]; + } @catch (NSException *e) { + self.error = [e debugDescription]; + } + } + NSString *type = json[@"type"]; + self.type = [CTInAppUtils inAppActionTypeFromString:type]; + self.fallbackToSettings = json[@"fbSettings"] ? [json[@"fbSettings"] boolValue] : NO; + self.customTemplateInAppData = [CTCustomTemplateInAppData createWithJSON:json]; + } @catch (NSException *e) { + self.error = [e debugDescription]; + } + } + return self; +} + +- (nonnull instancetype)initWithOpenURL:(nonnull NSURL *)url { + if (self = [super init]) { + self.type = CTInAppActionTypeOpenURL; + self.actionURL = url; + } + return self; +} + +@end diff --git a/CleverTapSDK/CTNotificationButton.h b/CleverTapSDK/CTNotificationButton.h index c1f46520..20af03e5 100644 --- a/CleverTapSDK/CTNotificationButton.h +++ b/CleverTapSDK/CTNotificationButton.h @@ -1,4 +1,6 @@ #import +#import "CTInAppUtils.h" +#import "CTNotificationAction.h" @interface CTNotificationButton : NSObject @@ -7,9 +9,11 @@ @property (nonatomic, copy, readonly) NSString *borderRadius; @property (nonatomic, copy, readonly) NSString *borderColor; @property (nonatomic, copy, readonly) NSDictionary *customExtras; -@property (nonatomic, copy, readonly) NSString *type; +@property (nonatomic, readonly) CTInAppActionType type; @property (nonatomic, readonly) BOOL fallbackToSettings; +@property (nonatomic, strong, readonly) CTNotificationAction *action; + @property (nonatomic, copy, readonly) NSString *backgroundColor; @property (nonatomic, readonly) NSURL *actionURL; diff --git a/CleverTapSDK/CTNotificationButton.m b/CleverTapSDK/CTNotificationButton.m index e50e53ea..1c902fd8 100644 --- a/CleverTapSDK/CTNotificationButton.m +++ b/CleverTapSDK/CTNotificationButton.m @@ -1,4 +1,5 @@ #import "CTNotificationButton.h" +#import "CTConstants.h" @interface CTNotificationButton () { @@ -9,10 +10,8 @@ @interface CTNotificationButton () { @property (nonatomic, copy, readwrite) NSString *borderRadius; @property (nonatomic, copy, readwrite) NSString *borderColor; @property (nonatomic, copy, readwrite) NSString *backgroundColor; -@property (nonatomic, copy, readwrite) NSDictionary *customExtras; -@property (nonatomic, copy, readwrite) NSString *type; -@property (nonatomic, readwrite) BOOL fallbackToSettings; -@property (nonatomic, readwrite) NSURL *actionURL; + +@property (nonatomic, strong, readwrite) CTNotificationAction *action; @property (nonatomic, copy, readwrite) NSDictionary *jsonDescription; @@ -32,21 +31,13 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject { self.borderColor = jsonObject[@"border"]; self.backgroundColor = jsonObject[@"bg"]; - NSDictionary *actions = jsonObject[@"actions"]; + NSDictionary *actions = jsonObject[CLTAP_INAPP_ACTIONS]; if (actions) { - self.customExtras = (NSDictionary *) actions[@"kv"]; - NSString *action = actions[@"ios"]; - if (action && action.length > 0) { - @try { - self.actionURL = [NSURL URLWithString:action]; - } @catch (NSException *e) { - self.error = [e debugDescription]; - } + self.action = [[CTNotificationAction alloc] initWithJSON:actions]; + if (self.action.error) { + self.error = self.action.error; } - self.type = actions[@"type"]; - self.fallbackToSettings = actions[@"fbSettings"] ? [actions[@"fbSettings"] boolValue] : NO; } - } @catch (NSException *e) { self.error = [e debugDescription]; } @@ -54,4 +45,20 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject { return self; } +- (NSDictionary *)customExtras { + return [self.action keyValues]; +} + +- (CTInAppActionType)type { + return [self.action type]; +} + +- (BOOL)fallbackToSettings { + return [self.action fallbackToSettings]; +} + +- (NSURL *)actionURL { + return [self.action actionURL]; +} + @end diff --git a/CleverTapSDK/CTProfileBuilder.h b/CleverTapSDK/CTProfileBuilder.h index 89765bb2..b3e93be5 100644 --- a/CleverTapSDK/CTProfileBuilder.h +++ b/CleverTapSDK/CTProfileBuilder.h @@ -23,4 +23,6 @@ + (void)buildDecrementValueBy:(NSNumber *_Nonnull)value forKey: (NSString *_Nonnull)key localDataStore:(CTLocalDataStore* _Nonnull)dataStore completionHandler:(void(^ _Nonnull )(NSDictionary *_Nullable operatorDict, NSNumber *_Nullable updatedValue, NSArray *_Nullable errors))completion; ++ (NSNumber *_Nullable)_getUpdatedValue:(NSNumber *_Nonnull)value forKey:(NSString *_Nonnull)key withCommand:(NSString *_Nonnull)command cachedValue:(id _Nullable)cachedValue; + @end diff --git a/CleverTapSDK/CTProfileBuilder.m b/CleverTapSDK/CTProfileBuilder.m index 65fc3454..e827a1c6 100644 --- a/CleverTapSDK/CTProfileBuilder.m +++ b/CleverTapSDK/CTProfileBuilder.m @@ -7,16 +7,6 @@ #import "CTLocalDataStore.h" #import "CTUtils.h" -// profile commands -static NSString *const kCLTAP_COMMAND_SET = @"$set"; -static NSString *const kCLTAP_COMMAND_ADD = @"$add"; -static NSString *const kCLTAP_COMMAND_REMOVE = @"$remove"; -static NSString *const kCLTAP_COMMAND_INCREMENT = @"$incr"; -static NSString *const kCLTAP_COMMAND_DECREMENT = @"$decr"; -static NSString *const kCLTAP_COMMAND_DELETE = @"$delete"; - -#define CLTAP_MULTIVAL_COMMANDS @[kCLTAP_COMMAND_SET, kCLTAP_COMMAND_ADD, kCLTAP_COMMAND_REMOVE] - @implementation CTProfileBuilder + (void)build:(NSDictionary *)profile completionHandler:(void(^ _Nonnull )(NSDictionary* _Nullable customFields, NSDictionary* _Nullable systemFields, NSArray* _Nullable errors))completion { @@ -392,11 +382,16 @@ + (void)_handleIncrementDecrementValue:(NSNumber *_Nonnull)value forKey:(NSStrin NSDictionary* operatorDict = @{ key: @{command: value} }; + id cachedValue = [dataStore getProfileFieldForKey: key]; NSNumber *newValue; + newValue = [self _getUpdatedValue:value forKey:key withCommand:command cachedValue:cachedValue]; - id cachedValue = [dataStore getProfileFieldForKey: key]; + completion(operatorDict, newValue, nil); +} + ++ (NSNumber *_Nullable)_getUpdatedValue:(NSNumber *_Nonnull)value forKey:(NSString *_Nonnull)key withCommand:(NSString *_Nonnull)command cachedValue:(id)cachedValue { + NSNumber *newValue; if ([cachedValue isKindOfClass: [NSNumber class]]) { - NSNumber *cachedNumber = (NSNumber*)cachedValue; CFNumberType numberType = CFNumberGetType((CFNumberRef)cachedNumber); @@ -449,9 +444,7 @@ + (void)_handleIncrementDecrementValue:(NSNumber *_Nonnull)value forKey:(NSStrin break; } } - - completion(operatorDict, newValue, nil); + return newValue; } - @end diff --git a/CleverTapSDK/CTRequestFactory.h b/CleverTapSDK/CTRequestFactory.h index 9586ba7b..634d71b7 100644 --- a/CleverTapSDK/CTRequestFactory.h +++ b/CleverTapSDK/CTRequestFactory.h @@ -3,7 +3,7 @@ // CleverTapSDK // // Created by Akash Malhotra on 09/01/23. -// Copyright © 2023 CleverTap. All rights reserved. +// Copyright © 2024 CleverTap. All rights reserved. // #import @@ -14,7 +14,9 @@ + (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; ++ (CTRequest *_Nonnull)syncVarsRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params domain:(NSString *_Nonnull)domain; ++ (CTRequest *_Nonnull)syncTemplatesRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params domain:(NSString *_Nonnull)domain; + @end diff --git a/CleverTapSDK/CTRequestFactory.m b/CleverTapSDK/CTRequestFactory.m index 5c2923c5..9edc1c49 100644 --- a/CleverTapSDK/CTRequestFactory.m +++ b/CleverTapSDK/CTRequestFactory.m @@ -3,7 +3,7 @@ // CleverTapSDK // // Created by Akash Malhotra on 09/01/23. -// Copyright © 2023 CleverTap. All rights reserved. +// Copyright © 2024 CleverTap. All rights reserved. // #import "CTRequestFactory.h" @@ -12,15 +12,25 @@ @implementation CTRequestFactory + (CTRequest *_Nonnull)helloRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config { - return [[CTRequest alloc]initWithHttpMethod:@"GET" config:config params:nil url:kHANDSHAKE_URL]; + 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]; + 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]; ++ (CTRequest *_Nonnull)syncVarsRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params domain:(NSString *_Nonnull)domain { + NSString *url = [self urlWithDomain:domain andPath:@"defineVars"]; + return [[CTRequest alloc] initWithHttpMethod:@"POST" config:config params: params url:url]; +} + ++ (CTRequest *_Nonnull)syncTemplatesRequestWithConfig:(CleverTapInstanceConfig *_Nonnull)config params:(id _Nullable)params domain:(NSString *_Nonnull)domain { + NSString *url = [self urlWithDomain:domain andPath:@"defineTemplates"]; + return [[CTRequest alloc] initWithHttpMethod:@"POST" config:config params: params url:url]; +} + ++ (NSString *)urlWithDomain:(NSString *)domain andPath:(NSString *)path { + return [NSString stringWithFormat:@"https://%@/%@", domain, path]; } @end diff --git a/CleverTapSDK/CTUIUtils.m b/CleverTapSDK/CTUIUtils.m index 37d729fb..1245d3ac 100644 --- a/CleverTapSDK/CTUIUtils.m +++ b/CleverTapSDK/CTUIUtils.m @@ -53,7 +53,7 @@ + (UIWindow * _Nullable)getKeyWindow { + (CGFloat)getLeftMargin { CGFloat margin = 0; - if (@available(iOS 11.0, *)) { + if (@available(iOS 11.0, tvOS 11.0, *)) { for (UIWindow *window in [CTUIUtils getSharedApplication].windows) { if (window.isKeyWindow) { margin = window.safeAreaInsets.left; diff --git a/CleverTapSDK/CTUserInfoMigrator.h b/CleverTapSDK/CTUserInfoMigrator.h new file mode 100644 index 00000000..5285a96a --- /dev/null +++ b/CleverTapSDK/CTUserInfoMigrator.h @@ -0,0 +1,14 @@ +// +// CTUserInfoMigrator.h +// Pods +// +// Created by Kushagra Mishra on 29/05/24. +// + +#import + +@interface CTUserInfoMigrator : NSObject + ++ (void)migrateUserInfoFileForAccountID:(NSString *)acc_id deviceID:(NSString *)device_id; + +@end diff --git a/CleverTapSDK/CTUserInfoMigrator.m b/CleverTapSDK/CTUserInfoMigrator.m new file mode 100644 index 00000000..cbc48cdc --- /dev/null +++ b/CleverTapSDK/CTUserInfoMigrator.m @@ -0,0 +1,26 @@ +#import "CTUserInfoMigrator.h" +#import "CTConstants.h" + +@implementation CTUserInfoMigrator + ++ (void)migrateUserInfoFileForAccountID:(NSString *)acc_id deviceID:(NSString *)device_id { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *libraryPath = [paths objectAtIndex:0]; + NSString *userProfileWithAccountID = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *userProfileWithAccountIDPath = [libraryPath stringByAppendingPathComponent:userProfileWithAccountID]; + NSString *userProfileWithAccountIDAndDeviceID = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *userProfileWithAccountIDAndDeviceIDPath = [libraryPath stringByAppendingPathComponent:userProfileWithAccountIDAndDeviceID]; + + NSError *error = nil; + if ([fileManager fileExistsAtPath:userProfileWithAccountIDPath]) { + [fileManager copyItemAtPath:userProfileWithAccountIDPath toPath:userProfileWithAccountIDAndDeviceIDPath error:&error]; + CleverTapLogStaticInternal(@"[CTUserInfo]: Local file copied successfully to %@", userProfileWithAccountIDAndDeviceIDPath); + [fileManager removeItemAtPath:userProfileWithAccountIDPath error:&error]; + return; + } else { + CleverTapLogStaticInternal(@"[CTUserInfo]: Failed to copy local file: %@", error.localizedDescription); + } +} + +@end diff --git a/CleverTapSDK/CTValidator.m b/CleverTapSDK/CTValidator.m index 314ede39..a5329611 100644 --- a/CleverTapSDK/CTValidator.m +++ b/CleverTapSDK/CTValidator.m @@ -185,7 +185,7 @@ + (CTValidationResult *)cleanObjectValue:(NSObject *)o context:(CTValidatorConte } return vr; } else if ([o isKindOfClass:[NSDate class]]) { - NSString *date = [NSString stringWithFormat:@"$D_%d", (int) ((NSDate *) o).timeIntervalSince1970]; + NSString *date = [NSString stringWithFormat:@"%@%d",CLTAP_DATE_PREFIX, (int) ((NSDate *) o).timeIntervalSince1970]; [vr setObject:date]; return vr; diff --git a/CleverTapSDK/CleverTap+CTVar.h b/CleverTapSDK/CleverTap+CTVar.h index acc9d89f..d6c1b058 100644 --- a/CleverTapSDK/CleverTap+CTVar.h +++ b/CleverTapSDK/CleverTap+CTVar.h @@ -52,6 +52,8 @@ NS_SWIFT_NAME(defineVar(name:unsignedLongLong:)); NS_SWIFT_NAME(defineVar(name:UnsignedShort:)); - (CTVar *)defineVar:(NSString *)name withDictionary:(nullable NSDictionary *)defaultValue NS_SWIFT_NAME(defineVar(name:dictionary:)); +- (CTVar *)defineFileVar:(NSString *)name +NS_SWIFT_NAME(defineFileVar(name:)); @end diff --git a/CleverTapSDK/CleverTap.h b/CleverTapSDK/CleverTap.h index c9e9081a..44070c58 100644 --- a/CleverTapSDK/CleverTap.h +++ b/CleverTapSDK/CleverTap.h @@ -24,6 +24,7 @@ @protocol CleverTapPushNotificationDelegate; #if !CLEVERTAP_NO_INAPP_SUPPORT @protocol CleverTapInAppNotificationDelegate; +@class CTTemplateContext; #endif @protocol CTBatchSentDelegate; @@ -1417,6 +1418,64 @@ extern NSString * _Nonnull const CleverTapProfileDidInitializeNotification; */ - (id _Nullable)getVariableValue:(NSString * _Nonnull)name; +/*! + @method + + @abstract + Adds a callback to be invoked when no more file downloads are pending (either when no files needed to be downloaded or all downloads have been completed). + + @param block a callback to add. + */ +- (void)onVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull )block; + +/*! + @method + + @abstract + Adds a callback to be invoked only once when no more file downloads are pending (either when no files needed to be downloaded or all downloads have been completed). + + @param block a callback to add. + */ +- (void)onceVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull )block; + +#if !CLEVERTAP_NO_INAPP_SUPPORT +#pragma mark Custom Templates and Functions + +/*! + @method + + @abstract + Uploads Custom in-app templates and app functions to the server. Requires Development/Debug build/configuration. + */ +- (void)syncCustomTemplates; + +/*! + @method + + @abstract + Uploads Custom in-app templates and app functions to the server. + + @param isProduction Provide `true` if Custom in-app templates and app functions must be sync in Productuon build/configuration. + */ +- (void)syncCustomTemplates:(BOOL)isProduction; + +/*! + @method + + @abstract + Retrieves the active context for a template that is currently displaying. If the provided template + name is not of a currently active template, this method returns nil. + + @param templateName The template name to get the active context for. + + @return + A CTTemplateContext object representing the active context for the given template name, or nil if no active context exists. + + */ +- (CTTemplateContext * _Nullable)activeContextForTemplate:(NSString * _Nonnull)templateName; + +#endif + @end #pragma clang diagnostic pop diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index cf86d25f..041e2a3f 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -31,6 +31,7 @@ #import "CTDispatchQueueManager.h" #import "CTMultiDelegateManager.h" #import "CTSessionManager.h" +#import "CTFileDownloader.h" #if !CLEVERTAP_NO_INAPP_SUPPORT #import "CTInAppFCManager.h" @@ -56,7 +57,7 @@ #import "CleverTap+InAppsResponseHandler.h" #import "CTInAppEvaluationManager.h" #import "CTInAppTriggerManager.h" -#import "CTInAppImagePrefetchManager.h" +#import "CTCustomTemplatesManager-Internal.h" #endif #if !CLEVERTAP_NO_INBOX_SUPPORT @@ -233,17 +234,19 @@ @interface CleverTap () { @property (atomic, weak) id batchSentDelegate; @property (nonatomic, strong, readwrite) CTMultiDelegateManager *delegateManager; +@property (nonatomic, strong, readwrite) CTFileDownloader *fileDownloader; + #if !CLEVERTAP_NO_INAPP_SUPPORT @property (atomic, weak) id pushPermissionDelegate; -@property (strong, nonatomic, nullable) CleverTapFetchInAppsBlock fetchInAppsBlock; @property (atomic, strong) CTPushPrimerManager *pushPrimerManager; +@property (strong, nonatomic, nullable) CleverTapFetchInAppsBlock fetchInAppsBlock; @property (nonatomic, strong, readwrite) CTInAppFCManager *inAppFCManager; @property (nonatomic, strong, readwrite) CTInAppEvaluationManager *inAppEvaluationManager; @property (nonatomic, strong, readwrite) CTInAppDisplayManager *inAppDisplayManager; @property (nonatomic, strong, readwrite) CTImpressionManager *impressionManager; -@property (nonatomic, strong, readwrite) CTInAppImagePrefetchManager *imagePrefetchManager; @property (nonatomic, strong, readwrite) CTInAppStore * _Nullable inAppStore; +@property (nonatomic, strong, readwrite) CTCustomTemplatesManager *customTemplatesManager; #endif @property (atomic, strong) NSString *processingLoginUserIdentifier; @@ -460,11 +463,12 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( if (_deviceInfo.timeZone&& ![_deviceInfo.timeZone isEqualToString:@""]) { initialProfileValues[CLTAP_SYS_TZ] = _deviceInfo.timeZone; } - _localDataStore = [[CTLocalDataStore alloc] initWithConfig:_config profileValues:initialProfileValues andDeviceInfo: _deviceInfo]; self.dispatchQueueManager = [[CTDispatchQueueManager alloc]initWithConfig:_config]; self.delegateManager = [[CTMultiDelegateManager alloc] init]; + _localDataStore = [[CTLocalDataStore alloc] initWithConfig:_config profileValues:initialProfileValues andDeviceInfo: _deviceInfo dispatchQueueManager:_dispatchQueueManager]; + _lastAppLaunchedTime = [self eventGetLastTime:@"App Launched"]; self.validationResultStack = [[CTValidationResultStack alloc]initWithConfig: _config]; self.userSetLocation = kCLLocationCoordinate2DInvalid; @@ -477,6 +481,8 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( [self initNetworking]; [self inflateQueuesAsync]; [self addObservers]; + + self.fileDownloader = [[CTFileDownloader alloc] initWithConfig:self.config]; #if !CLEVERTAP_NO_INAPP_SUPPORT if (!_config.analyticsOnly && ![CTUIUtils runningInsideAppExtension]) { [self initializeInAppSupport]; @@ -496,7 +502,7 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( [self _initProductConfig]; // Initialise Variables - self.variables = [[CTVariables alloc] initWithConfig:self.config deviceInfo:self.deviceInfo]; + self.variables = [[CTVariables alloc] initWithConfig:self.config deviceInfo:self.deviceInfo fileDownloader:self.fileDownloader]; [self notifyUserProfileInitialized]; } @@ -506,12 +512,8 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( #if !CLEVERTAP_NO_INAPP_SUPPORT - (void)initializeInAppSupport { - CTInAppImagePrefetchManager *imagePrefetchManager = [[CTInAppImagePrefetchManager alloc] initWithConfig:self.config]; - self.imagePrefetchManager = imagePrefetchManager; - CTInAppStore *inAppStore = [[CTInAppStore alloc] initWithConfig:self.config delegateManager:self.delegateManager - imagePrefetchManager:self.imagePrefetchManager deviceId:self.deviceInfo.deviceId]; self.inAppStore = inAppStore; @@ -520,15 +522,19 @@ - (void)initializeInAppSupport { CTInAppFCManager *inAppFCManager = [[CTInAppFCManager alloc] initWithConfig:self.config delegateManager:self.delegateManager deviceId:[_deviceInfo.deviceId copy] impressionManager:impressionManager inAppTriggerManager:triggerManager]; + CTCustomTemplatesManager *templatesManager = [[CTCustomTemplatesManager alloc] initWithConfig:self.config]; + CTInAppDisplayManager *displayManager = [[CTInAppDisplayManager alloc] initWithCleverTap:self dispatchQueueManager:self.dispatchQueueManager inAppFCManager:inAppFCManager impressionManager:impressionManager inAppStore:inAppStore - imagePrefetchManager:self.imagePrefetchManager]; + templatesManager:templatesManager + fileDownloader:self.fileDownloader]; CTInAppEvaluationManager *evaluationManager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.config.accountId deviceId:self.deviceInfo.deviceId delegateManager:self.delegateManager impressionManager:impressionManager inAppDisplayManager:displayManager inAppStore:inAppStore inAppTriggerManager:triggerManager]; + self.customTemplatesManager = templatesManager; self.inAppFCManager = inAppFCManager; self.impressionManager = impressionManager; self.inAppEvaluationManager = evaluationManager; @@ -731,6 +737,21 @@ - (void)doHandshakeAsyncWithCompletion:(void (^ _Nullable )(void))taskBlock { }]; } +- (void)runSerialAsyncEnsureHandshake:(void(^)(void))block { + if ([self needHandshake]) { + [self.dispatchQueueManager runSerialAsync:^{ + [self doHandshakeAsyncWithCompletion:^{ + block(); + }]; + }]; + } + else { + [self.dispatchQueueManager runSerialAsync:^{ + block(); + }]; + } +} + - (BOOL)updateStateFromResponseHeadersShouldRedirectForNotif:(NSDictionary *)headers { CleverTapLogInternal(self.config.logLevel, @"%@: processing response with headers:%@", self, headers); BOOL shouldRedirect = NO; @@ -1563,8 +1584,13 @@ - (void)resumeInAppNotifications { } - (void)clearInAppResources:(BOOL)expiredOnly { - [self.imagePrefetchManager _clearImageAssets:expiredOnly]; + [self.fileDownloader clearFileAssets:expiredOnly]; +} + +- (CTTemplateContext * _Nullable)activeContextForTemplate:(NSString * _Nonnull)templateName { + return [[self customTemplatesManager] activeContextForTemplate:templateName]; } + #endif #pragma mark Private Method @@ -1945,7 +1971,7 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp #if !CLEVERTAP_NO_INAPP_SUPPORT // Evaluate the event only if it will be processed [self.dispatchQueueManager runSerialAsync:^{ - [self evaluateOnEvent:event]; + [self evaluateOnEvent:event withType: eventType]; }]; #endif @@ -1960,16 +1986,19 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp } } -- (void)evaluateOnEvent:(NSDictionary *)event { +- (void)evaluateOnEvent:(NSDictionary *)event withType:(CleverTapEventType)eventType { +#if !CLEVERTAP_NO_INAPP_SUPPORT NSString *eventName = event[CLTAP_EVENT_NAME]; // Add the system properties for evaluation NSMutableDictionary *eventData = [[NSMutableDictionary alloc] initWithDictionary:[self generateAppFields]]; // Add the event properties last, so custom properties are not overriden [eventData addEntriesFromDictionary:event[CLTAP_EVENT_DATA]]; -#if !CLEVERTAP_NO_INAPP_SUPPORT if (eventName && [eventName isEqualToString:CLTAP_CHARGED_EVENT]) { NSArray *items = eventData[CLTAP_CHARGED_EVENT_ITEMS]; [self.inAppEvaluationManager evaluateOnChargedEvent:eventData andItems:items]; + } else if (eventType == CleverTapEventTypeProfile) { + NSDictionary *> *result = [self.localDataStore getUserAttributeChangeProperties:event]; + [self.inAppEvaluationManager evaluateOnUserAttributeChange:result]; } else if (eventName) { [self.inAppEvaluationManager evaluateOnEvent:eventName withProps:eventData]; } @@ -2210,7 +2239,7 @@ - (void)sendQueue:(NSMutableArray *)queue ofType:(CTQueueType)queueType { [self scheduleQueueFlush]; [self handleSendQueueFail]; - [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:NO]; + [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:NO withQueueType:queueType]; } if (!success || redirect) { @@ -2223,7 +2252,7 @@ - (void)sendQueue:(NSMutableArray *)queue ofType:(CTQueueType)queueType { [self parseResponse:responseData]; - [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:YES]; + [self.delegateManager notifyDelegatesBatchDidSend:batchWithHeader withSuccess:YES withQueueType:queueType]; CleverTapLogDebug(self.config.logLevel,@"%@: Successfully sent %lu events", self, (unsigned long)[batch count]); @@ -2558,9 +2587,6 @@ - (void) _asyncSwitchUser:(NSDictionary *)properties withCachedGuid:(NSString *) // clear ARP and other context for the old user [self clearUserContext]; - // clear old profile data - [self.localDataStore changeUser]; - [self.sessionManager resetSession]; if (cachedGUID) { @@ -2571,6 +2597,9 @@ - (void) _asyncSwitchUser:(NSDictionary *)properties withCachedGuid:(NSString *) [self.deviceInfo forceNewDeviceID]; } + // clear old profile data + [self.localDataStore changeUser]; + [self recordDeviceErrors]; [[self delegateManager] notifyDelegatesDeviceIdDidChange:self.deviceInfo.deviceId]; @@ -2727,17 +2756,14 @@ - (void)onUserLogin:(NSDictionary *_Nonnull)properties withCleverTapID:(NSString - (void)profilePush:(NSDictionary *)properties { [self.dispatchQueueManager runSerialAsync:^{ [CTProfileBuilder build:properties completionHandler:^(NSDictionary *customFields, NSDictionary *systemFields, NSArray*errors) { + NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; if (systemFields) { - [self.localDataStore setProfileFields:systemFields]; + [profile addEntriesFromDictionary:systemFields]; } - NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; if (customFields) { - CleverTapLogInternal(self.config.logLevel, @"%@: Constructed custom profile: %@", self, customFields); - [self.localDataStore setProfileFields:customFields]; [profile addEntriesFromDictionary:customFields]; } [self cacheGUIDSforProfile:profile]; - #if !defined(CLEVERTAP_TVOS) // make sure Phone is a string and debug check for country code and phone format, but always send NSArray *profileAllKeys = [profile allKeys]; @@ -2777,6 +2803,10 @@ - (NSString *)profileGetCleverTapID { return self.deviceInfo.deviceId; } +- (id)profileGetLocalValues:(NSString *)propertyName { + return [self.localDataStore getProfileFieldForKey:propertyName]; +} + - (NSString *)getAccountID { return self.config.accountId; } @@ -2803,7 +2833,6 @@ - (void)profileRemoveValueForKey:(NSString *)key { NSMutableDictionary *profile = [[self.localDataStore generateBaseProfile] mutableCopy]; NSString* _key = [customFields allKeys][0]; CleverTapLogInternal(self.config.logLevel, @"%@: removing key %@ from profile", self, _key); - [self.localDataStore removeProfileFieldForKey:_key]; [profile addEntriesFromDictionary:customFields]; NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; @@ -2894,8 +2923,6 @@ - (void)_handleIncrementDecrementProfilePushForKey:(NSString *)key updatedValue: [profile addEntriesFromDictionary:operatorDict]; CleverTapLogInternal(self.config.logLevel, @"Created Increment/ Decrement profile push: %@", operatorDict); - [self.localDataStore setProfileFieldWithKey: key andValue: updatedValue]; - NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; event[@"profile"] = profile; [self queueEvent:event withType:CleverTapEventTypeProfile]; @@ -2923,7 +2950,6 @@ - (void)_handleMultiValueProfilePush:(NSDictionary*)customFields updatedMultiVal } } - #pragma mark - User Action Events API - (void)recordEvent:(NSString *)event { @@ -4272,88 +4298,96 @@ + (BOOL)isValidCleverTapId:(NSString *_Nullable)cleverTapID { return [CTValidator isValidCleverTapId:cleverTapID]; } -#pragma mark - Product Experiences +#pragma mark - Sync PE and Custom Templates -- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { - [[self variables] onVariablesChanged:block]; +- (void)syncVariables { + [self syncVariables:NO]; } -- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { - [[self variables] onceVariablesChanged:block]; +- (void)syncVariables:(BOOL)isProduction { + NSDictionary *varsPayload = [[self variables] varsPayload]; + [self syncWithBlock:^{ + NSDictionary *meta = [self batchHeaderForQueue:CTQueueTypeUndefined]; + CTRequest *ctRequest = [CTRequestFactory syncVarsRequestWithConfig:self.config params:@[meta, varsPayload] domain:self.domainFactory.redirectDomain]; + [self syncRequest:ctRequest logMessage:@"Vars sync"]; + } methodName:NSStringFromSelector(_cmd) isProduction:isProduction]; } -- (void)syncVariables { - [self syncVariables:NO]; +#if !CLEVERTAP_NO_INAPP_SUPPORT +- (void)syncCustomTemplates { + [self syncCustomTemplates:NO]; } -- (void)syncVariablesEnsureHandshake { - if ([self needHandshake]) { - [self.dispatchQueueManager runSerialAsync:^{ - [self doHandshakeAsyncWithCompletion:^{ - [self _syncVars]; - }]; - }]; - } - else { - [self.dispatchQueueManager runSerialAsync:^{ - [self _syncVars]; - }]; - } +- (void)syncCustomTemplates:(BOOL)isProduction { + NSDictionary *syncPayload = [[self customTemplatesManager] syncPayload]; + [self syncWithBlock:^{ + NSDictionary *meta = [self batchHeaderForQueue:CTQueueTypeUndefined]; + CTRequest *ctRequest = [CTRequestFactory syncTemplatesRequestWithConfig:self.config params:@[meta, syncPayload] domain:self.domainFactory.redirectDomain]; + [self syncRequest:ctRequest logMessage:@"Define Custom Templates"]; + } methodName:NSStringFromSelector(_cmd) isProduction:isProduction]; } - -- (void)syncVariables:(BOOL)isProduction { +#endif + +- (void)syncWithBlock:(void(^)(void))syncBlock methodName:(NSString *)methodName isProduction:(BOOL)isProduction { if (isProduction) { #if DEBUG - CleverTapLogInfo(_config.logLevel, @"%@: Calling syncVariables: with isProduction:YES from Debug configuration/build. Use syncVariables in this case", self); + CleverTapLogInfo(_config.logLevel, @"%@: Calling %@: with isProduction:YES from Debug configuration/build. Use syncVariables in this case", self, methodName); #else - CleverTapLogInfo(_config.logLevel, @"%@: Calling syncVariables: with isProduction:YES from Release configuration/build. Do not release this build and use with caution", self); + CleverTapLogInfo(_config.logLevel, @"%@: Calling %@: with isProduction:YES from Release configuration/build. Do not release this build and use with caution", self, methodName); #endif - [self syncVariablesEnsureHandshake]; + [self runSerialAsyncEnsureHandshake:syncBlock]; } else { #if DEBUG - [self syncVariablesEnsureHandshake]; + [self runSerialAsyncEnsureHandshake:syncBlock]; #else - CleverTapLogInfo(_config.logLevel, @"%@: syncVariables can only be called from Debug configurations/builds", self); + CleverTapLogInfo(_config.logLevel, @"%@: %@ can only be called from Debug configurations/builds", self, methodName); #endif } } -- (void)_syncVars { - NSDictionary *meta = [self batchHeaderForQueue:CTQueueTypeUndefined]; - NSDictionary *varsPayload = [[self variables] varsPayload]; - NSArray *payload = @[meta,varsPayload]; - +- (void)syncRequest:(CTRequest *)request logMessage:(NSString *)logMessage { 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 + [request onResponse:^(NSData * _Nullable data, NSURLResponse * _Nullable response) { + [self handleSyncOnResponse:data response:response logMessage:logMessage]; dispatch_semaphore_signal(semaphore); }]; - [ctRequest onError:^(NSError * _Nullable error) { - CleverTapLogDebug(self->_config.logLevel, @"%@: error syncing vars: %@", self, error.debugDescription); + [request onError:^(NSError * _Nullable error) { + CleverTapLogDebug(self->_config.logLevel, @"%@: Error %@: %@", self, logMessage, error.debugDescription); dispatch_semaphore_signal(semaphore); }]; - [self.requestSender send:ctRequest]; + [self.requestSender send:request]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } +- (void)handleSyncOnResponse:(NSData * _Nullable)data response:(NSURLResponse * _Nullable)response + logMessage:(NSString *)logMessage { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + CleverTapLogDebug(self->_config.logLevel, @"%@: %@ successful.", self, logMessage); + } + 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: %@", self, logMessage, jsonResp[@"error"]); + } + CT_END_TRY +} + +#pragma mark - Product Experiences + +- (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onVariablesChanged:block]; +} + +- (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onceVariablesChanged:block]; +} + - (void)fetchVariables:(CleverTapFetchVariablesBlock)block { [[self variables] setFetchVariablesBlock:block]; [self queueEvent:@{CLTAP_EVENT_NAME: CLTAP_WZRK_FETCH_EVENT, CLTAP_EVENT_DATA: @{@"t": @4}} withType:CleverTapEventTypeFetch]; @@ -4371,6 +4405,14 @@ - (id _Nullable)getVariableValue:(NSString * _Nonnull)name { return [[self.variables varCache] getMergedValue:name]; } +- (void)onVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onVariablesChangedAndNoDownloadsPending:block]; +} + +- (void)onceVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull )block { + [[self variables] onceVariablesChangedAndNoDownloadsPending:block]; +} + #pragma mark - PE Vars - (CTVar *)defineVar:(NSString *)name { @@ -4482,4 +4524,8 @@ - (CTVar *)defineVar:(NSString *)name withDictionary:(NSDictionary *)defaultValu return [self.variables define:name with:defaultValue kind:CT_KIND_DICTIONARY]; } +- (CTVar *)defineFileVar:(NSString *)name { + return [self.variables define:name with:nil kind:CT_KIND_FILE]; +} + @end diff --git a/CleverTapSDK/CleverTapBuildInfo.h b/CleverTapSDK/CleverTapBuildInfo.h index d967e764..88724c9b 100644 --- a/CleverTapSDK/CleverTapBuildInfo.h +++ b/CleverTapSDK/CleverTapBuildInfo.h @@ -1 +1 @@ -#define WR_SDK_REVISION @"60201" +#define WR_SDK_REVISION @"70000" diff --git a/CleverTapSDK/CleverTapInternal.h b/CleverTapSDK/CleverTapInternal.h index a458c0e9..9befcbaf 100644 --- a/CleverTapSDK/CleverTapInternal.h +++ b/CleverTapSDK/CleverTapInternal.h @@ -8,6 +8,7 @@ #import "CTSessionManager.h" @class CTInAppDisplayManager; +@class CTFileDownloader; @interface CleverTap (Internal) @@ -31,8 +32,11 @@ typedef NS_ENUM(NSInteger, CleverTapEventType) { @property (nonatomic, assign, readonly) BOOL isAppForeground; @property (nonatomic, strong, readonly) CTDeviceInfo * _Nonnull deviceInfo; @property (atomic, strong, readonly) CTSessionManager * _Nonnull sessionManager; +@property (nonatomic, strong, readonly) CTCustomTemplatesManager * _Nullable customTemplatesManager; #endif +@property (nonatomic, strong, readonly) CTFileDownloader * _Nullable fileDownloader; + + (NSMutableDictionary * _Nullable)getInstances; - (void)recordInAppNotificationStateEvent:(BOOL)clicked diff --git a/CleverTapSDK/CleverTapJSInterface.h b/CleverTapSDK/CleverTapJSInterface.h index 29da114e..8211d027 100644 --- a/CleverTapSDK/CleverTapJSInterface.h +++ b/CleverTapSDK/CleverTapJSInterface.h @@ -1,18 +1,16 @@ #import #if !(TARGET_OS_TV) #import -@class CleverTapInstanceConfig; +@class CleverTapInstanceConfig; +@class CTInAppDisplayViewController; /*! - @abstract The `CleverTapJSInterface` class is a bridge to communicate between Webviews and CleverTap SDK. Calls to forward record events or set user properties fired within a Webview to CleverTap SDK. */ @interface CleverTapJSInterface : NSObject -@property (nonatomic, strong) WKUserContentController *userContentController; - - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; @end diff --git a/CleverTapSDK/CleverTapJSInterface.m b/CleverTapSDK/CleverTapJSInterface.m index 31f847a2..4609998e 100644 --- a/CleverTapSDK/CleverTapJSInterface.m +++ b/CleverTapSDK/CleverTapJSInterface.m @@ -4,9 +4,18 @@ #import "CleverTapInstanceConfigPrivate.h" #import "CleverTapJSInterfacePrivate.h" +#import "CTConstants.h" +#import "CTNotificationAction.h" +#import "CTInAppDisplayViewController.h" + +#import "CleverTapBuildInfo.h" + @interface CleverTapJSInterface (){} @property (nonatomic, strong) CleverTapInstanceConfig *config; +// The controller initializes the CleverTapJSInterface and retains it hence this property needs to be weak +@property (nonatomic, weak) CTInAppDisplayViewController *controller; + @end @implementation CleverTapJSInterface @@ -15,22 +24,22 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config { if (self = [super init]) { _config = config; _wv_init = YES; - [self initUserContentController]; } return self; } -- (instancetype)initWithConfigForInApps:(CleverTapInstanceConfig *)config { +- (instancetype)initWithConfigForInApps:(CleverTapInstanceConfig *)config fromController:(CTInAppDisplayViewController *)controller { if (self = [super init]) { _config = config; - [self initUserContentController]; + _controller = controller; } return self; } -- (void)initUserContentController { - _userContentController = [[WKUserContentController alloc] init]; - [_userContentController addScriptMessageHandler:self name:@"clevertap"]; +- (WKUserScript *)versionScript { + NSString *js = [NSString stringWithFormat:@"window.cleverTapIOSSDKVersion = %@;", WR_SDK_REVISION]; + WKUserScript *wkScript = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; + return wkScript; } - (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { @@ -68,15 +77,34 @@ - (void)handleMessageFromWebview:(NSDictionary *)message forInsta [cleverTap profileRemoveMultiValue: message[@"value"] forKey: message[@"key"]]; } else if ([action isEqual: @"profileRemoveMultiValues"]) { [cleverTap profileRemoveMultiValues: message[@"values"] forKey: message[@"key"]]; - } - else if ([action isEqual: @"recordChargedEvent"]) { + } else if ([action isEqual: @"recordChargedEvent"]) { [cleverTap recordChargedEventWithDetails: message[@"chargeDetails"] andItems: message[@"items"]]; - }else if ([action isEqual: @"onUserLogin"]) { + } else if ([action isEqual: @"onUserLogin"]) { [cleverTap onUserLogin: message[@"properties"]]; - }else if ([action isEqual: @"profileIncrementValueBy"]) { + } else if ([action isEqual: @"profileIncrementValueBy"]) { [cleverTap profileIncrementValueBy: message[@"value"] forKey: message[@"key"]]; - }else if ([action isEqual: @"profileDecrementValueBy"]) { + } else if ([action isEqual: @"profileDecrementValueBy"]) { [cleverTap profileDecrementValueBy: message[@"value"] forKey: message[@"key"]]; + } else if ([action isEqual: @"triggerInAppAction"]) { + [self triggerInAppAction:message[@"actionJson"] callToAction:message[@"callToAction"] buttonId:message[@"buttonId"]]; + } +} + +- (void)triggerInAppAction:(NSDictionary *)actionJson callToAction:(NSString *)callToAction buttonId:(NSString *)buttonId { + if (!actionJson) { + CleverTapLogDebug(self.config.logLevel, @"%@: action JSON is nil.", [self class]); + return; + } + if (!self.controller) { + CleverTapLogDebug(self.config.logLevel, @"%@: display view controller is nil.", [self class]); + return; + } + + CTNotificationAction *action = [[CTNotificationAction alloc] initWithJSON:actionJson]; + if (action && !action.error) { + [self.controller triggerInAppAction:action callToAction:callToAction buttonId:buttonId]; + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: error creating action from action JSON: %@.", [self class], action.error); } } diff --git a/CleverTapSDK/CleverTapJSInterfacePrivate.h b/CleverTapSDK/CleverTapJSInterfacePrivate.h index d3881561..19e3a773 100644 --- a/CleverTapSDK/CleverTapJSInterfacePrivate.h +++ b/CleverTapSDK/CleverTapJSInterfacePrivate.h @@ -1,8 +1,11 @@ #import @interface CleverTapJSInterface () {} -- (instancetype)initWithConfigForInApps:(CleverTapInstanceConfig *)config; +- (instancetype)initWithConfigForInApps:(CleverTapInstanceConfig *)config fromController:(CTInAppDisplayViewController *)controller; // SET ONLY WHEN THE USER INITIALISES A WEBVIEW WITH CT JS INTERFACE @property (nonatomic, assign) BOOL wv_init; + +- (WKUserScript *)versionScript; + @end diff --git a/CleverTapSDK/FileDownload/CTFileDownloadManager.h b/CleverTapSDK/FileDownload/CTFileDownloadManager.h new file mode 100644 index 00000000..6df6e703 --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloadManager.h @@ -0,0 +1,68 @@ +#import + +@class CleverTapInstanceConfig; + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^CTFilesDownloadCompletedBlock)(NSDictionary *status); +typedef void(^CTFilesDeleteCompletedBlock)(NSDictionary *status); +typedef void(^CTFilesRemoveCompletedBlock)(NSDictionary *status); +typedef void (^DownloadCompletionHandler)(NSURL *url, BOOL success); + +@interface CTFileDownloadManager : NSObject + ++ (instancetype)sharedInstanceWithConfig:(CleverTapInstanceConfig *)config; +- (instancetype)init NS_UNAVAILABLE; + +/*! + @method + + @discussion + This method accepts file urls as NSArray of NSURLs and downloads each file to directory inside NSDocumentDirectory directory. + + @param completion the completion block to be executed when all files are downloaded. `status` dictionary will + contain file download status of each file as {url,success}. The completion block is executed on background queue. + */ +- (void)downloadFiles:(NSArray *)urls withCompletionBlock:(CTFilesDownloadCompletedBlock)completion; + +/*! + @method + + @discussion + This method checks if file is already present in the directory or not. + */ +- (BOOL)isFileAlreadyPresent:(NSURL *)url; + +/*! + @method + + @discussion + This method returns the file path to the file. This method does *not* check if the file exists. + */ +- (NSString *)filePath:(NSURL *)url; + +/*! + @method + + @discussion + This method deletes the files from the directory if present. + + @param completion the completion block to be executed when all files are deleted. `status` dictionary will + contain file delete status of each file as {url,success}. The completion block is executed on background queue. + */ +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion; + +/*! + @method + + @discussion + This method deletes all files from the directory. + + @param completion the completion block to be executed when all files are deleted. `status` dictionary will + contain file download status of each file as {file path,success}. The completion block is executed on background queue. + */ +- (void)removeAllFilesWithCompletionBlock:(CTFilesRemoveCompletedBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/FileDownload/CTFileDownloadManager.m b/CleverTapSDK/FileDownload/CTFileDownloadManager.m new file mode 100644 index 00000000..3854e8eb --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloadManager.m @@ -0,0 +1,292 @@ +#import "CTFileDownloadManager.h" +#import "CTConstants.h" +#import "CleverTapInstanceConfig.h" + +@interface CTFileDownloadManager() + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) NSString *documentsDirectory; +@property (nonatomic, strong) NSMutableSet *downloadInProgressUrls; +@property (nonatomic, strong) NSMutableDictionary *> *downloadInProgressHandlers; +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSFileManager* fileManager; +@property NSTimeInterval semaphoreTimeout; + +@end + +@implementation CTFileDownloadManager + ++ (instancetype)sharedInstanceWithConfig:(CleverTapInstanceConfig *)config { + static CTFileDownloadManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initWithConfig:config]; + }); + return sharedInstance; +} + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config { + self = [super init]; + if (self) { + self.config = config; + NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + self.documentsDirectory = [documents stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + + _downloadInProgressUrls = [NSMutableSet new]; + _downloadInProgressHandlers = [NSMutableDictionary new]; + + NSURLSessionConfiguration *sc = [NSURLSessionConfiguration defaultSessionConfiguration]; + sc.timeoutIntervalForRequest = CLTAP_REQUEST_TIME_OUT_INTERVAL; + sc.timeoutIntervalForResource = CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL; + // Timeout of (request timeout + 5) seconds for acquiring semaphore + self.semaphoreTimeout = CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL + 5; + + self.session = [NSURLSession sessionWithConfiguration:sc]; + self.fileManager = [NSFileManager defaultManager]; + } + + return self; +} + +#pragma mark - Public methods +- (void)downloadFiles:(nonnull NSArray *)urls + withCompletionBlock:(nonnull CTFilesDownloadCompletedBlock)completion { + dispatch_group_t group = dispatch_group_create(); + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(CLTAP_FILE_MAX_CONCURRENCY_COUNT); + + NSMutableDictionary *filesDownloadStatus = [NSMutableDictionary new]; + for (NSURL *url in urls) { + dispatch_group_enter(group); + + dispatch_time_t semaphore_timeout = dispatch_time(DISPATCH_TIME_NOW, + self.semaphoreTimeout * NSEC_PER_SEC); + if (dispatch_semaphore_wait(semaphore, semaphore_timeout) != 0) { + @synchronized (filesDownloadStatus) { + [filesDownloadStatus setObject:@0 forKey:[url absoluteString]]; + } + dispatch_group_leave(group); + continue; // Proceed to next URL + } + + @synchronized (self) { + BOOL isAlreadyDownloading = [_downloadInProgressUrls containsObject:url]; + if (isAlreadyDownloading) { + // If the url download is already in Progress, add the completion handler to + // fileDownloadProgressHandlers dictionary which is called when file is downloaded. + if (!_downloadInProgressHandlers[url]) { + _downloadInProgressHandlers[url] = [NSMutableArray array]; + } + [_downloadInProgressHandlers[url] addObject:^(NSURL *completedURL, BOOL success) { + @synchronized (filesDownloadStatus) { + [filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[completedURL absoluteString]]; + } + dispatch_group_leave(group); + dispatch_semaphore_signal(semaphore); // Signal that a slot is free + }]; + continue; + } + } + + // Download file only when it is not already present. + if (![self isFileAlreadyPresent:url]) { + @synchronized (self) { + [_downloadInProgressUrls addObject:url]; + } + dispatch_async(concurrentQueue, ^{ + [self downloadSingleFile:url completed:^(BOOL success) { + @synchronized (filesDownloadStatus) { + [filesDownloadStatus setObject:[NSNumber numberWithBool:success] forKey:[url absoluteString]]; + } + // Call the other completion handlers for this file url if present + NSArray *handlers; + @synchronized (self) { + handlers = [self->_downloadInProgressHandlers[url] copy]; + [self->_downloadInProgressHandlers removeObjectForKey:url]; + [self->_downloadInProgressUrls removeObject:url]; + } + for (DownloadCompletionHandler handler in handlers) { + handler(url, success); + } + + dispatch_group_leave(group); + dispatch_semaphore_signal(semaphore); // Signal that a slot is free + }]; + }); + } else { + @synchronized (filesDownloadStatus) { + // Add the file url to callback as success true as it is already present + [filesDownloadStatus setObject:@1 forKey:[url absoluteString]]; + } + dispatch_group_leave(group); + dispatch_semaphore_signal(semaphore); // Signal that a slot is free + } + } + + dispatch_group_notify(group, concurrentQueue, ^{ + // Callback when all files are downloaded with their success status + completion(filesDownloadStatus); + }); +} + +- (BOOL)isFileAlreadyPresent:(NSURL *)url { + NSString* filePath = [self filePath:url]; + BOOL fileExists = [self.fileManager fileExistsAtPath:filePath]; + return fileExists; +} + +- (NSString *)filePath:(NSURL *)url { + return [self.documentsDirectory stringByAppendingPathComponent:[self hashedFileNameForURL:url]]; +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + NSMutableDictionary *filesDeleteStatus = [NSMutableDictionary new]; + + if (urls.count == 0) { + completion(filesDeleteStatus); + return; + } + + dispatch_group_t deleteGroup = dispatch_group_create(); + dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + for (NSString *urlString in urls) { + NSURL *url = [NSURL URLWithString:urlString]; + // Delete file only when it is present. + if ([self isFileAlreadyPresent:url]) { + dispatch_group_enter(deleteGroup); + dispatch_async(deleteConcurrentQueue, ^{ + [self deleteSingleFile:url completed:^(BOOL success) { + @synchronized(filesDeleteStatus) { + [filesDeleteStatus setObject:[NSNumber numberWithBool:success] forKey:urlString]; + } + dispatch_group_leave(deleteGroup); + }]; + }); + } else { + @synchronized(filesDeleteStatus) { + // Add the file url to callback as success true as it is already not present + [filesDeleteStatus setObject:@1 forKey:[url absoluteString]]; + } + } + } + dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ + // Callback when all files are deleted with their success status + completion(filesDeleteStatus); + }); +} + +- (void)removeAllFilesWithCompletionBlock:(CTFilesRemoveCompletedBlock)completion { + dispatch_group_t deleteGroup = dispatch_group_create(); + dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSMutableDictionary *filesDeleteStatus = [NSMutableDictionary new]; + NSError *error; + NSURL *path = [NSURL fileURLWithPath:self.documentsDirectory]; + NSArray *files = [self.fileManager contentsOfDirectoryAtURL:path + includingPropertiesForKeys:nil + options:NSDirectoryEnumerationSkipsHiddenFiles + error:&error]; + if (error) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to get contents of directory %@ - %@", self, path, error); + completion(filesDeleteStatus); + return; + } + for (NSURL *file in files) { + dispatch_group_enter(deleteGroup); + dispatch_async(deleteConcurrentQueue, ^{ + NSError *error; + BOOL success = [self.fileManager removeItemAtURL:file error:&error]; + if (!success) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to remove file %@ - %@", self, [file absoluteString], error); + } + // Synchronize access to the dictionary + @synchronized (filesDeleteStatus) { + [filesDeleteStatus setObject:@(success) forKey:[file path]]; + } + dispatch_group_leave(deleteGroup); + }); + } + dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ + // Callback when all files are deleted with their success status + completion(filesDeleteStatus); + }); +} + +#pragma mark - Private methods + +- (void)downloadSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock { + NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url + completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (error) { + CleverTapLogInternal(self.config.logLevel, @"%@ Error downloading file: %@ - %@", self, url, error); + completedBlock(NO); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode != 200) { + CleverTapLogInternal(self.config.logLevel, @"HTTP Error: %ld for file: %@", (long)httpResponse.statusCode, url); + completedBlock(NO); + return; + } + + NSFileManager *fileManager = self.fileManager; + NSError *createDirectoryError; + if (![fileManager fileExistsAtPath:self.documentsDirectory]) { + [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:self.documentsDirectory] + withIntermediateDirectories:YES + attributes:nil + error:&createDirectoryError]; + if (createDirectoryError) { + CleverTapLogInternal(self.config.logLevel, @"Error creating documents folder: %@", createDirectoryError); + completedBlock(NO); + return; + } + } + + NSString *destinationPath = [self filePath:url]; + NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath]; + NSError *fileError; + // Check if the file exists at the destination + if ([fileManager fileExistsAtPath:[destinationURL path]]) { + // Remove the existing file + if (![fileManager removeItemAtURL:destinationURL error:&fileError]) { + CleverTapLogInternal(self.config.logLevel, @"Error removing file at destination: %@", fileError); + completedBlock(NO); + return; + } + } + // Move the file from the temporary location to the documents directory + [fileManager moveItemAtURL:location toURL:destinationURL error:&fileError]; + if (fileError) { + CleverTapLogInternal(self.config.logLevel, @"File Error: %@ for file: %@", fileError.localizedDescription, url); + completedBlock(NO); + return; + } + completedBlock(YES); + }]; + [downloadTask resume]; +} + +- (NSString *)hashedFileNameForURL:(NSURL *)url { + NSUInteger hashValue = [url.absoluteString hash]; + return [NSString stringWithFormat:@"%lu_%@", (unsigned long)hashValue, [url lastPathComponent]]; +} + +- (void)deleteSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock { + if (![url lastPathComponent] || [[url lastPathComponent] isEqualToString:@""]) { + completedBlock(NO); + return; + } + + NSString *filePath = [self filePath:url]; + NSError *error; + BOOL success = [self.fileManager removeItemAtPath:filePath error:&error]; + if (!success) { + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to remove file %@ - %@", self, url, error); + } + completedBlock(success); +} + +@end diff --git a/CleverTapSDK/FileDownload/CTFileDownloader.h b/CleverTapSDK/FileDownload/CTFileDownloader.h new file mode 100644 index 00000000..97886581 --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloader.h @@ -0,0 +1,20 @@ +#import +#import + +@class CleverTapInstanceConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloader : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^ _Nullable)(NSDictionary *status))completion; +- (BOOL)isFileAlreadyPresent:(NSString *)url andUpdateExpiryTime:(BOOL)updateExpiryTime; +- (void)clearFileAssets:(BOOL)expiredOnly; +- (nullable NSString *)fileDownloadPath:(NSString *)url; +- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/FileDownload/CTFileDownloader.m b/CleverTapSDK/FileDownload/CTFileDownloader.m new file mode 100644 index 00000000..2a95b15e --- /dev/null +++ b/CleverTapSDK/FileDownload/CTFileDownloader.m @@ -0,0 +1,265 @@ +#import "CTFileDownloader.h" +#import "CTConstants.h" +#import "CTPreferences.h" +#import "CTFileDownloadManager.h" +#if !CLEVERTAP_NO_INAPP_SUPPORT +#import +#endif + +@interface CTFileDownloader() + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSMutableDictionary *urlsExpiry; +@property (nonatomic) NSTimeInterval fileExpiryTime; + +@end + +@implementation CTFileDownloader + +- (nonnull instancetype)initWithConfig:(nonnull CleverTapInstanceConfig *)config { + self = [super init]; + if (self) { + self.config = config; + [self setup]; + } + return self; +} + +#pragma mark - Public + +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^ _Nullable)(NSDictionary *status))completion { + if (fileURLs.count == 0) { + if (completion) { + completion(@{}); + } + return; + } + + NSArray *urls = [self fileURLs:fileURLs]; + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary *status) { + [self updateFilesExpiry:status]; + [self updateFilesExpiryInPreference]; + + long lastDeletedTime = [self lastDeletedTimestamp]; + [self removeInactiveExpiredAssets:lastDeletedTime]; + + if (completion) { + completion(status); + } + }]; +} + +- (BOOL)isFileAlreadyPresent:(NSString *)url + andUpdateExpiryTime:(BOOL)updateExpiryTime { + NSURL *fileUrl = [NSURL URLWithString:url]; + BOOL fileExists = [self.fileDownloadManager isFileAlreadyPresent:fileUrl]; + + // For PE File vars when fileExists returns true, we are not calling downloadFiles for existing files + // so we need to update file expiry time for those existing files. + if (fileExists && updateExpiryTime) { + [self updateFilesExpiry:@{url:@1}]; + [self updateFilesExpiryInPreference]; + } + return fileExists; +} + +- (void)clearFileAssets:(BOOL)expiredOnly { + if (expiredOnly) { + // Disregard the last deleted timestamp to force delete the expired files + // currentTime - lastDeletedTime > self.fileExpiryTime + long forceLastDeleted = ([self currentTimeInterval] - self.fileExpiryTime) - 1; + [self removeInactiveExpiredAssets:forceLastDeleted]; + } else { + [self removeAllAssetsWithCompletion:nil]; + } +} + +- (nullable NSString *)fileDownloadPath:(NSString *)url { + if ([self isFileAlreadyPresent:url andUpdateExpiryTime:NO]) { + NSURL *fileURL = [NSURL URLWithString:url]; + return [self.fileDownloadManager filePath:fileURL]; + } else { + CleverTapLogInternal(self.config.logLevel, @"%@ File %@ is not present.", self, url); + } + return nil; +} + +- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL { + NSURL *URL = [NSURL URLWithString:imageURL]; + NSString *imagePath = [self.fileDownloadManager filePath:URL]; + UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imagePath]]; + if (image) { + return image; + } + + CleverTapLogInternal(self.config.logLevel, @"%@ Failed to load image from path %@", self, imagePath); + return nil; +} + +#pragma mark - Private + +- (void)setup { + self.fileDownloadManager = [CTFileDownloadManager sharedInstanceWithConfig:self.config]; + self.fileExpiryTime = CLTAP_FILE_EXPIRY_OFFSET; + +#if !CLEVERTAP_NO_INAPP_SUPPORT + [self removeLegacyAssets:nil]; +#endif + + @synchronized (self) { + NSDictionary *cachedUrlsExpiry = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + if (cachedUrlsExpiry) { + self.urlsExpiry = [cachedUrlsExpiry mutableCopy]; + } else { + self.urlsExpiry = [NSMutableDictionary new]; + } + } + + // Check for expired files and delete them at every start of session. + long lastDeletedTime = [self lastDeletedTimestamp]; + [self removeInactiveExpiredAssets:lastDeletedTime]; +} + +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { + if (lastDeletedTime > 0) { + long currentTime = [self currentTimeInterval]; + if (currentTime - lastDeletedTime > self.fileExpiryTime) { + NSMutableArray *inactiveUrls = [NSMutableArray new]; + for (NSString *key in self.urlsExpiry) { + long expiry = [self.urlsExpiry[key] longValue]; + if (currentTime > expiry) { + [inactiveUrls addObject:key]; + } + } + + [self deleteFiles:inactiveUrls withCompletionBlock:nil]; + } + } +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + [self.fileDownloadManager deleteFiles:urls withCompletionBlock:^(NSDictionary *status) { + [self removeDeletedFilesFromExpiry:status]; + [self updateFilesExpiryInPreference]; + [self updateLastDeletedTimestamp]; + if (completion) { + completion(status); + } + }]; +} + +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion { + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + [self.urlsExpiry removeAllObjects]; + [self updateFilesExpiryInPreference]; + [self updateLastDeletedTimestamp]; + if (completion) { + completion(status); + } + CleverTapLogInternal(self.config.logLevel, @"%@ Remove all files completed with status: %@", self, status); + }]; +} + +- (void)removeDeletedFilesFromExpiry:(NSDictionary *)status { + @synchronized (self) { + for (NSString *key in status) { + if ([status[key] boolValue]) { + [self.urlsExpiry removeObjectForKey:key]; + } + } + } +} + +- (NSArray *)fileURLs:(NSArray *)fileURLs { + NSMutableSet *urls = [NSMutableSet new]; + for (NSString *urlString in fileURLs) { + NSURL *url = [NSURL URLWithString:urlString]; + [urls addObject:url]; + } + return [urls allObjects]; +} + +- (void)updateFilesExpiry:(NSDictionary *)status { + @synchronized (self) { + NSNumber *expiry = @([self currentTimeInterval] + self.fileExpiryTime); + for (NSString *key in status) { + // Update the expiry for urls with success status + if ([status[key] boolValue]) { + self.urlsExpiry[key] = expiry; + } + } + } +} + +- (void)updateFilesExpiryInPreference { + @synchronized (self) { + [CTPreferences putObject:self.urlsExpiry forKey:[self storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + } +} + +- (long)lastDeletedTimestamp { + long lastDeletedTime = [CTPreferences getIntForKey:[self storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS] + withResetValue:[self currentTimeInterval]]; + return lastDeletedTime; +} + +- (void)updateLastDeletedTimestamp { + [CTPreferences putInt:[self currentTimeInterval] + forKey:[self storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; +} + +- (NSString *)storageKeyWithSuffix:(NSString *)suffix { + return [NSString stringWithFormat:@"%@:%@", self.config.accountId, suffix]; +} + +- (long)currentTimeInterval { + return [[NSDate date] timeIntervalSince1970]; +} + +#if !CLEVERTAP_NO_INAPP_SUPPORT +- (void)removeLegacyAssets:(void (^)(void))completion { + NSArray *activeAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + NSArray *inactiveAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + NSMutableSet *urls = [NSMutableSet new]; + if (activeAssetsArray && activeAssetsArray.count > 0) { + [urls addObjectsFromArray:activeAssetsArray]; + } + if (inactiveAssetsArray && inactiveAssetsArray.count > 0) { + [urls addObjectsFromArray:inactiveAssetsArray]; + } + + if (!inactiveAssetsArray && !activeAssetsArray) { + return; + } + + dispatch_group_t deleteGroup = dispatch_group_create(); + dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + SDImageCache *sdImageCache = [SDImageCache sharedImageCache]; + for (NSString *url in urls) { + dispatch_group_enter(deleteGroup); + dispatch_async(deleteConcurrentQueue, ^{ + if ([sdImageCache diskImageDataExistsWithKey:url]) { + [sdImageCache removeImageForKey:url + fromDisk:YES + withCompletion:^{ + dispatch_group_leave(deleteGroup); + }]; + } else { + dispatch_group_leave(deleteGroup); + } + }); + } + + dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; + if (completion) { + completion(); + } + }); +} +#endif + +@end diff --git a/CleverTapSDK/InApps/CTAlertViewController.m b/CleverTapSDK/InApps/CTAlertViewController.m index 490cab1c..9f496fec 100644 --- a/CleverTapSDK/InApps/CTAlertViewController.m +++ b/CleverTapSDK/InApps/CTAlertViewController.m @@ -118,8 +118,8 @@ - (void)showFromWindow:(BOOL)animated { [self.window setHidden:NO]; void (^completionBlock)(void) = ^ { - if (self.delegate && [self.delegate respondsToSelector:@selector(notificationDidShow:fromViewController:)]) { - [self.delegate notificationDidShow:self.notification fromViewController:self]; + if (self.delegate) { + [self.delegate notificationDidShow:self.notification]; } }; diff --git a/CleverTapSDK/InApps/CTBaseHeaderFooterViewController.m b/CleverTapSDK/InApps/CTBaseHeaderFooterViewController.m index fd84f2e9..84f6f43a 100644 --- a/CleverTapSDK/InApps/CTBaseHeaderFooterViewController.m +++ b/CleverTapSDK/InApps/CTBaseHeaderFooterViewController.m @@ -86,8 +86,8 @@ - (void)setUpImage { // set image if (self.notification.inAppImage) { self.inAppImage = self.notification.inAppImage; - } else if (self.notification.image) { - self.inAppImage = [UIImage imageWithData:self.notification.image]; + } else if (self.notification.imageData) { + self.inAppImage = [UIImage imageWithData:self.notification.imageData]; } if (self.inAppImage) { self.imageView.clipsToBounds = YES; @@ -381,8 +381,8 @@ - (void)showFromWindow:(BOOL)animated { [self.window setHidden:NO]; void (^completionBlock)(void) = ^ { - if (self.delegate && [self.delegate respondsToSelector:@selector(notificationDidShow:fromViewController:)]) { - [self.delegate notificationDidShow:self.notification fromViewController:self]; + if (self.delegate) { + [self.delegate notificationDidShow:self.notification]; } }; if (animated) { diff --git a/CleverTapSDK/InApps/CTCoverViewController.m b/CleverTapSDK/InApps/CTCoverViewController.m index c982748d..150deeb1 100644 --- a/CleverTapSDK/InApps/CTCoverViewController.m +++ b/CleverTapSDK/InApps/CTCoverViewController.m @@ -74,14 +74,14 @@ - (void)layoutNotification { if (![self deviceOrientationIsLandscape]) { if (self.notification.inAppImage) { self.imageView.image = self.notification.inAppImage; - } else if (self.notification.image) { - self.imageView.image = [UIImage imageWithData:self.notification.image]; + } else if (self.notification.imageData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageData]; } } else { if (self.notification.inAppImageLandscape) { self.imageView.image = self.notification.inAppImageLandscape; - } else if (self.notification.imageLandscape) { - self.imageView.image = [UIImage imageWithData:self.notification.imageLandscape]; + } else if (self.notification.imageLandscapeData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageLandscapeData]; } } diff --git a/CleverTapSDK/InApps/CTHalfInterstitialViewController.m b/CleverTapSDK/InApps/CTHalfInterstitialViewController.m index 0b272062..d54201ae 100644 --- a/CleverTapSDK/InApps/CTHalfInterstitialViewController.m +++ b/CleverTapSDK/InApps/CTHalfInterstitialViewController.m @@ -118,14 +118,14 @@ - (void)layoutNotification { if (![self deviceOrientationIsLandscape]) { if (self.notification.inAppImage) { self.imageView.image = self.notification.inAppImage; - } else if (self.notification.image) { - self.imageView.image = [UIImage imageWithData:self.notification.image]; + } else if (self.notification.imageData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageData]; } } else { if (self.notification.inAppImageLandscape) { self.imageView.image = self.notification.inAppImageLandscape; - } else if (self.notification.imageLandscape) { - self.imageView.image = [UIImage imageWithData:self.notification.imageLandscape]; + } else if (self.notification.imageLandscapeData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageLandscapeData]; } } diff --git a/CleverTapSDK/InApps/CTImageInAppViewController.m b/CleverTapSDK/InApps/CTImageInAppViewController.m index e75c4512..e95e6add 100644 --- a/CleverTapSDK/InApps/CTImageInAppViewController.m +++ b/CleverTapSDK/InApps/CTImageInAppViewController.m @@ -120,14 +120,14 @@ - (void)setUpImage { if (![self deviceOrientationIsLandscape]) { if (self.notification.inAppImage) { self.imageView.image = self.notification.inAppImage; - } else if (self.notification.image) { - self.imageView.image = [UIImage imageWithData:self.notification.image]; + } else if (self.notification.imageData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageData]; } } else { if (self.notification.inAppImageLandscape) { self.imageView.image = self.notification.inAppImageLandscape; - } else if (self.notification.imageLandscape) { - self.imageView.image = [UIImage imageWithData:self.notification.imageLandscape]; + } else if (self.notification.imageLandscapeData) { + self.imageView.image = [UIImage imageWithData:self.notification.imageLandscapeData]; } } } diff --git a/CleverTapSDK/InApps/CTInAppDisplayManager.h b/CleverTapSDK/InApps/CTInAppDisplayManager.h index e64e9c9d..fefa8bfc 100644 --- a/CleverTapSDK/InApps/CTInAppDisplayManager.h +++ b/CleverTapSDK/InApps/CTInAppDisplayManager.h @@ -13,7 +13,8 @@ #import "CleverTap.h" #import "CTPushPrimerManager.h" #import "CTInAppStore.h" -#import "CTInAppImagePrefetchManager.h" +#import "CTFileDownloader.h" +#import "CTCustomTemplatesManager.h" NS_ASSUME_NONNULL_BEGIN @@ -36,11 +37,13 @@ typedef NS_ENUM(NSInteger, CleverTapInAppRenderingStatus) { inAppFCManager:(CTInAppFCManager *)inAppFCManager impressionManager:(CTImpressionManager *)impressionManager inAppStore:(CTInAppStore *)inAppStore - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager; + templatesManager:(CTCustomTemplatesManager *)templatesManager + fileDownloader:(CTFileDownloader *)fileDownloader; - (void)setPushPrimerManager:(CTPushPrimerManager* _Nonnull)pushPrimerManagerObj; - (void)prepareNotificationForDisplay:(NSDictionary* _Nonnull)jsonObj; - (BOOL)didHandleInAppTestFromPushNotificaton:(NSDictionary* _Nullable)notification; +- (BOOL)isTemplateRegistered:(NSDictionary *)inAppJSON; - (void)_addInAppNotificationsToQueue:(NSArray *)inappNotifs; - (void)_showNotificationIfAvailable; diff --git a/CleverTapSDK/InApps/CTInAppDisplayManager.m b/CleverTapSDK/InApps/CTInAppDisplayManager.m index a09db86d..f5255add 100644 --- a/CleverTapSDK/InApps/CTInAppDisplayManager.m +++ b/CleverTapSDK/InApps/CTInAppDisplayManager.m @@ -35,14 +35,36 @@ #import "CTLocalInApp.h" #import "CleverTap+PushPermission.h" #import "CleverTapJSInterfacePrivate.h" -#import "CTInAppImagePrefetchManager.h" + +#import "CTCustomTemplatesManager-Internal.h" +#import "CTCustomTemplateInAppData-Internal.h" +#endif + +#if !(TARGET_OS_TV) +#import +#import #endif static const void *const kNotificationQueueKey = &kNotificationQueueKey; +static const NSString *kInAppNotificationKey = @"inAppNotification"; // static here as we may have multiple instances handling inapps static CTInAppDisplayViewController *currentDisplayController; -static NSMutableArray *pendingNotificationControllers; +static CTInAppNotification *currentlyDisplayingNotification; +static NSMutableArray *pendingNotifications; + +// private class +@interface ImageLoadingResult : NSObject + +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) NSData *imageData; +@property (nonatomic, copy) NSString *error; + +@end + +@implementation ImageLoadingResult + +@end @interface CTInAppDisplayManager() { } @@ -57,7 +79,9 @@ @interface CTInAppDisplayManager() { @property (nonatomic, weak) CleverTap* instance; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; + +@property (nonatomic, strong) CTCustomTemplatesManager *templatesManager; @property (nonatomic, strong, readonly) NSString *imageInterstitialHtml; @@ -70,7 +94,7 @@ @implementation CTInAppDisplayManager + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - pendingNotificationControllers = [NSMutableArray new]; + pendingNotifications = [NSMutableArray new]; }); } @@ -79,18 +103,31 @@ - (instancetype _Nonnull)initWithCleverTap:(CleverTap * _Nonnull)instance inAppFCManager:(CTInAppFCManager *)inAppFCManager impressionManager:(CTImpressionManager *)impressionManager inAppStore:(CTInAppStore *)inAppStore - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager { + templatesManager:(CTCustomTemplatesManager *)templatesManager + fileDownloader:(CTFileDownloader *)fileDownloader { if ((self = [super init])) { self.dispatchQueueManager = dispatchQueueManager; self.instance = instance; self.config = instance.config; self.inAppFCManager = inAppFCManager; self.inAppStore = inAppStore; - self.imagePrefetchManager = imagePrefetchManager; + self.templatesManager = templatesManager; + self.fileDownloader = fileDownloader; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onDisplayPendingNotification:) + name:[self.class pendingNotificationKey:self.config.accountId] + object:nil]; } return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:[self.class pendingNotificationKey:self.config.accountId] + object:nil]; +} + - (void)setPushPrimerManager:(CTPushPrimerManager *)pushPrimerManagerObj { pushPrimerManager = pushPrimerManagerObj; } @@ -118,7 +155,8 @@ - (void)setInAppNotificationDelegate:(id )de - (void)_addInAppNotificationsToQueue:(NSArray *)inappNotifs { @try { - [self.inAppStore enqueueInApps:inappNotifs]; + NSArray *filteredInAppNotifs = [self filterNonRegisteredTemplates:inappNotifs]; + [self.inAppStore enqueueInApps:filteredInAppNotifs]; // Fire the first notification, if any [self.dispatchQueueManager runOnNotificationQueue:^{ @@ -129,6 +167,46 @@ - (void)_addInAppNotificationsToQueue:(NSArray *)inappNotifs { } } +- (void)_addInAppNotificationInFrontOfQueue:(CTInAppNotification *)inappNotif { + @try { + NSString *templateName = inappNotif.customTemplateInAppData.templateName; + if ([self.templatesManager isRegisteredTemplateWithName:templateName]) { + [self.inAppStore insertInFrontInApp:inappNotif.jsonDescription]; + // Fire the first notification, if any + [self.dispatchQueueManager runOnNotificationQueue:^{ + [self _showNotificationIfAvailable]; + }]; + } + } @catch (NSException *e) { + CleverTapLogInternal(self.config.logLevel, @"%@: InApp notification handling error: %@", self, e.debugDescription); + } +} + +- (NSArray *)filterNonRegisteredTemplates:(NSArray *)inappNotifs { + NSMutableArray *filteredInAppNotifs = [NSMutableArray new]; + for (NSDictionary *inAppJSON in inappNotifs) { + if ([self isTemplateRegistered:inAppJSON]) { + [filteredInAppNotifs addObject:inAppJSON]; + } + } + return filteredInAppNotifs; +} + +- (BOOL)isTemplateRegistered:(NSDictionary *)inAppJSON { + CTCustomTemplateInAppData *customTemplateData = [CTCustomTemplateInAppData createWithJSON:inAppJSON]; + + if (customTemplateData) { + if ([self.templatesManager isRegisteredTemplateWithName:customTemplateData.templateName]) { + return YES; + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: Template with name: \"%@\" is not registered and cannot be presented.", self, customTemplateData.templateName); + return NO; + } + } else { + return YES; + } +} + - (void)_showInAppNotificationIfAny { if ([CTUIUtils runningInsideAppExtension]){ CleverTapLogDebug(self.config.logLevel, @"%@: showInAppNotificationIfAny is a no-op in an app extension.", self); @@ -208,7 +286,7 @@ - (void)prepareNotificationForDisplay:(NSDictionary*)jsonObj { [self.dispatchQueueManager runOnNotificationQueue:^{ CleverTapLogInternal(self.config.logLevel, @"%@: processing inapp notification: %@", self, jsonObj); - __block CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:jsonObj imagePrefetchManager:self.imagePrefetchManager]; + __block CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:jsonObj]; if (notification.error) { CleverTapLogInternal(self.config.logLevel, @"%@: unable to parse inapp notification: %@ error: %@", self, jsonObj, notification.error); return; @@ -219,8 +297,8 @@ - (void)prepareNotificationForDisplay:(NSDictionary*)jsonObj { CleverTapLogInternal(self.config.logLevel, @"%@: InApp has elapsed its time to live, not showing the InApp: %@ wzrk_ttl: %lu", self, jsonObj, (unsigned long)notification.timeToLive); return; } - - [notification prepareWithCompletionHandler:^{ + + [self prepareNotification:notification withCompletion:^{ [CTUtils runSyncMainQueue:^{ [self notificationReady:notification]; }]; @@ -228,6 +306,74 @@ - (void)prepareNotificationForDisplay:(NSDictionary*)jsonObj { }]; } +- (void)prepareNotification:(CTInAppNotification *)notification withCompletion:(void (^)(void))completionHandler { +#if !(TARGET_OS_TV) + if ([NSThread isMainThread]) { + notification.error = [NSString stringWithFormat:@"[%@ prepareWithCompletionHandler] should not be called on the main thread", [self class]]; + completionHandler(); + return; + } + + if (notification.imageURL) { + ImageLoadingResult *result = [self loadImageWithURL:notification.imageURL contentType:notification.contentType]; + [notification setPreparedInAppImage:result.image inAppImageData:result.imageData error:result.error]; + } + if (notification.imageUrlLandscape && notification.hasLandscape) { + ImageLoadingResult *result = [self loadImageWithURL:notification.imageUrlLandscape contentType:notification.landscapeContentType]; + [notification setPreparedInAppImageLandscape:result.image inAppImageLandscapeData:result.imageData error:result.error]; + } + + NSArray *urls = [[self.templatesManager fileArgsURLsForInAppData:notification.customTemplateInAppData] allObjects]; + if (urls.count > 0) { + [self.fileDownloader downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nonnull status) { + for (NSString *url in status) { + if (![status[url] boolValue]) { + notification.error = @"Failed to download custom template files."; + break; + } + } + completionHandler(); + }]; + return; + } + +#endif + + completionHandler(); +} + +- (ImageLoadingResult *)loadImageWithURL:(NSURL *)url contentType:(NSString *)contentType { + ImageLoadingResult *result = [[ImageLoadingResult alloc] init]; + + UIImage *loadedImage = [self loadImageIfPresentInDiskCache:url]; + if (loadedImage) { + result.image = loadedImage; + } else { + NSError *loadError = nil; + NSData *imageData = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&loadError]; + if (loadError || !imageData) { + result.error = [NSString stringWithFormat:@"unable to load image from URL: %@", url]; + } else { + if ([contentType isEqualToString:@"image/gif"]) { + SDAnimatedImage *gif = [SDAnimatedImage imageWithData:imageData]; + if (gif == nil) { + result.error = [NSString stringWithFormat:@"unable to decode gif for URL: %@", url]; + } + } + result.imageData = imageData; + } + } + + return result; +} + +- (UIImage *)loadImageIfPresentInDiskCache:(NSURL *)imageURL { + NSString *imageURLString = [imageURL absoluteString]; + UIImage *image = [self.fileDownloader loadImageFromDisk:imageURLString]; + if (image) return image; + return nil; +} + - (void)notificationReady:(CTInAppNotification*)notification { if (![NSThread isMainThread]) { [CTUtils runSyncMainQueue:^{ @@ -264,6 +410,15 @@ - (void)displayNotification:(CTInAppNotification*)notification { return; } + // if we are currently displaying a notification, cache this notification for later display + if (currentlyDisplayingNotification) { + if (self.config.accountId && notification) { + [pendingNotifications addObject:@[self.config.accountId, notification]]; + CleverTapLogDebug(self.config.logLevel, @"%@: InApp already displaying, queueing to pending InApps", self); + } + return; + } + BOOL isHTMLType = (notification.inAppType == CTInAppTypeHTML); BOOL isInternetAvailable = self.instance.deviceInfo.isOnline; if (isHTMLType && !isInternetAvailable) { @@ -285,12 +440,9 @@ - (void)displayNotification:(CTInAppNotification*)notification { CTInAppDisplayViewController *controller; NSString *errorString = nil; - CleverTapJSInterface *jsInterface = nil; - switch (notification.inAppType) { case CTInAppTypeHTML: - jsInterface = [[CleverTapJSInterface alloc] initWithConfigForInApps:self.config]; - controller = [[CTInAppHTMLViewController alloc] initWithNotification:notification jsInterface:jsInterface]; + controller = [[CTInAppHTMLViewController alloc] initWithNotification:notification config:self.config]; break; case CTInAppTypeInterstitial: controller = [[CTInterstitialViewController alloc] initWithNotification:notification]; @@ -319,11 +471,22 @@ - (void)displayNotification:(CTInAppNotification*)notification { case CTInAppTypeCoverImage: controller = [[CTCoverImageViewController alloc] initWithNotification:notification]; break; + case CTInAppTypeCustom: + if ([self.templatesManager presentNotification:notification + withDelegate:self + andFileDownloader:self.fileDownloader]) { + currentlyDisplayingNotification = notification; + } else { + errorString = [NSString stringWithFormat:@"Cannot present custom notification with template name: %@.", + notification.customTemplateInAppData.templateName]; + } + break; default: errorString = [NSString stringWithFormat:@"Unhandled notification type: %lu", (unsigned long)notification.inAppType]; break; } if (controller) { + currentlyDisplayingNotification = notification; CleverTapLogDebug(self.config.logLevel, @"%@: Will show new InApp: %@", self, notification.campaignId); controller.delegate = self; [[self class] displayInAppDisplayController:controller]; @@ -351,34 +514,48 @@ - (void)notifyNotificationDismissed:(CTInAppNotification *)notification { } } +#pragma mark - Pending Notification + +- (void)onDisplayPendingNotification:(NSNotification *)notification { + currentlyDisplayingNotification = nil; + CleverTapLogDebug(self.config.logLevel, @"%@: Displaying pending notification.", self); + [self displayNotification:notification.userInfo[kInAppNotificationKey]]; +} + #pragma mark - InAppDisplayController static + (void)displayInAppDisplayController:(CTInAppDisplayViewController*)controller { - // if we are currently displaying a notification, cache this notification for later display - if (currentDisplayController) { - [pendingNotificationControllers addObject:controller]; - return; - } - // no current notification so display currentDisplayController = controller; [controller show:YES]; } + (void)inAppDisplayControllerDidDismiss:(CTInAppDisplayViewController*)controller { - if (currentDisplayController && currentDisplayController == controller) { - currentDisplayController = nil; + if (controller) { + if (currentDisplayController && currentDisplayController == controller) { + currentDisplayController = nil; + [self checkPendingNotifications]; + } + } else { [self checkPendingNotifications]; } } // static display handling as we may have more than one instance competing to show an inapp + (void)checkPendingNotifications { - if (pendingNotificationControllers && [pendingNotificationControllers count] > 0) { - CTInAppDisplayViewController *controller = [pendingNotificationControllers objectAtIndex:0]; - [pendingNotificationControllers removeObjectAtIndex:0]; - [self displayInAppDisplayController:controller]; + if (pendingNotifications && [pendingNotifications count] > 0) { + NSArray *pendingNotification = [pendingNotifications objectAtIndex:0]; + [pendingNotifications removeObjectAtIndex:0]; + NSString *accountId = [pendingNotification objectAtIndex:0]; + CTInAppNotification *inAppNotification = [pendingNotification objectAtIndex:1]; + [[NSNotificationCenter defaultCenter] postNotificationName:[self pendingNotificationKey:accountId] object:nil userInfo:@{kInAppNotificationKey: inAppNotification}]; + } else { + currentlyDisplayingNotification = nil; } } ++ (NSString *)pendingNotificationKey:(NSString *)accountId { + return [NSString stringWithFormat:@"%@:%@:onDisplayPendingNotification", [self class], accountId]; +} + #pragma mark - CTInAppNotificationDisplayDelegate - (void)notificationDidDismiss:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller { @@ -388,7 +565,7 @@ - (void)notificationDidDismiss:(CTInAppNotification*)notification fromViewContro [self _showInAppNotificationIfAny]; } -- (void)notificationDidShow:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller { +- (void)notificationDidShow:(CTInAppNotification*)notification { CleverTapLogInternal(self.config.logLevel, @"%@: InApp did show: %@", self, notification.campaignId); [self.instance recordInAppNotificationStateEvent:NO forNotification:notification andQueryParameters:nil]; [self.inAppFCManager didShow:notification]; @@ -400,18 +577,45 @@ - (void)notifyNotificationButtonTappedWithCustomExtras:(NSDictionary *)customExt } } -- (void)handleNotificationCTA:(NSURL *)ctaURL buttonCustomExtras:(NSDictionary *)buttonCustomExtras forNotification:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller withExtras:(NSDictionary*)extras { - CleverTapLogInternal(self.config.logLevel, @"%@: handle InApp cta: %@ button custom extras: %@ with options:%@", self, ctaURL.absoluteString, buttonCustomExtras, extras); +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras { + CleverTapLogInternal(self.config.logLevel, @"%@: handle InApp action type:%@ with cta: %@ button custom extras: %@ with options:%@", self, [CTInAppUtils inAppActionTypeString:action.type], action.actionURL.absoluteString, action.keyValues, extras); + // record the notification clicked event [self.instance recordInAppNotificationStateEvent:YES forNotification:notification andQueryParameters:extras]; + + // add the action extras so they can be passed to the dismissedWithExtras delegate if (extras) { notification.actionExtras = extras; } - if (buttonCustomExtras && buttonCustomExtras.count > 0) { - CleverTapLogDebug(self.config.logLevel, @"%@: InApp: button tapped with custom extras: %@", self, buttonCustomExtras); - [self notifyNotificationButtonTappedWithCustomExtras:buttonCustomExtras]; + + switch (action.type) { + case CTInAppActionTypeUnknown: + CleverTapLogDebug(self.config.logLevel, @"%@: Triggered in-app action with unknown type.", self); + break; + case CTInAppActionTypeClose: + // SDK in-apps are dismissed in CTInAppDisplayViewController buttonTapped: or tappedDismiss + if (notification.inAppType == CTInAppTypeCustom) { + [self.templatesManager closeNotification:notification]; + } + break; + case CTInAppActionTypeOpenURL: + [self handleCTAOpenURL:action.actionURL]; + break; + case CTInAppActionTypeKeyValues: + if (action.keyValues && action.keyValues.count > 0) { + CleverTapLogDebug(self.config.logLevel, @"%@: InApp: button tapped with custom extras: %@", self, action.keyValues); + [self notifyNotificationButtonTappedWithCustomExtras:action.keyValues]; + } + break; + case CTInAppActionTypeCustom: + [self triggerCustomTemplateAction:action.customTemplateInAppData forNotification:notification]; + break; + case CTInAppActionTypeRequestForPermission: + // Handled in CTInAppDisplayViewController handleButtonClickFromIndex: + break; } - else if (ctaURL) { - +} + +- (void)handleCTAOpenURL:(NSURL *)ctaURL { #if !CLEVERTAP_NO_INAPP_SUPPORT if (self.instance.urlDelegate) { // URL DELEGATE FOUND. OPEN DEEP LINKS ONLY IF USER ALLOWS IT @@ -428,8 +632,59 @@ - (void)handleNotificationCTA:(NSURL *)ctaURL buttonCustomExtras:(NSDictionary * }]; } #endif +} + +- (void)triggerCustomTemplateAction:(CTCustomTemplateInAppData *)actionData forNotification:(CTInAppNotification *)notification { + if (actionData && actionData.templateName) { + if ([self.templatesManager isRegisteredTemplateWithName:actionData.templateName]) { + CTCustomTemplateInAppData *inAppData = [actionData copy]; + [inAppData setIsAction:YES]; + CTInAppNotification *notificationFromAction = [self createNotificationForAction:inAppData andParentNotification:notification]; + + if ([self.templatesManager isVisualTemplateWithName:inAppData.templateName]) { + [self _addInAppNotificationInFrontOfQueue:notificationFromAction]; + } else { + NSSet *fileURLs = [self.templatesManager fileArgsURLsForInAppData:notificationFromAction.customTemplateInAppData]; + [self.fileDownloader downloadFiles:[fileURLs allObjects] withCompletionBlock:^(NSDictionary * _Nonnull status) { + for (NSString *url in status) { + if (![status[url] boolValue]) { + CleverTapLogDebug(self.config.logLevel, @"%@: Cannot trigger action due to file download error.", [self class]); + return; + } + } + [CTUtils runSyncMainQueue:^{ + [self.templatesManager presentNotification:notificationFromAction withDelegate:self andFileDownloader:self.fileDownloader]; + }]; + }]; + } + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: Cannot trigger non-registered template with name: %@", [self class], actionData.templateName); + } + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: Cannot trigger action without template name.", [self class]); } - [controller hide:true]; +} + +- (CTInAppNotification *)createNotificationForAction:(CTCustomTemplateInAppData *)inAppData andParentNotification:(CTInAppNotification *)notification { + NSMutableDictionary *json = [@{ + CLTAP_INAPP_ID: notification.Id ? notification.Id : @"", + CLTAP_PROP_WZRK_ID: notification.campaignId ? notification.campaignId : @"", + CLTAP_INAPP_TYPE: [CTInAppUtils inAppTypeString:CTInAppTypeCustom], + CLTAP_INAPP_EXCLUDE_GLOBAL_CAPS: @(YES), + CLTAP_INAPP_EXCLUDE_FROM_CAPS: @(YES), + CLTAP_INAPP_TTL: @(notification.timeToLive) + } mutableCopy]; + + if (notification.jsonDescription[CLTAP_NOTIFICATION_PIVOT]) { + json[CLTAP_NOTIFICATION_PIVOT] = notification.jsonDescription[CLTAP_NOTIFICATION_PIVOT]; + } + if (notification.jsonDescription[CLTAP_NOTIFICATION_CONTROL_GROUP_ID]) { + json[CLTAP_NOTIFICATION_CONTROL_GROUP_ID] = notification.jsonDescription[CLTAP_NOTIFICATION_CONTROL_GROUP_ID]; + } + + [json addEntriesFromDictionary:inAppData.json]; + + return [[CTInAppNotification alloc] initWithJSON:json]; } - (void)handleInAppPushPrimer:(CTInAppNotification *)notification diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.h b/CleverTapSDK/InApps/CTInAppEvaluationManager.h index 4b06163d..2dbea791 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.h +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.h @@ -34,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)evaluateOnEvent:(NSString *)eventName withProps:(NSDictionary *)properties; - (void)evaluateOnChargedEvent:(NSDictionary *)chargeDetails andItems:(NSArray *)items; +- (void)evaluateOnUserAttributeChange:(NSDictionary *)properties; - (void)evaluateOnAppLaunchedClientSide; - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs; diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.m b/CleverTapSDK/InApps/CTInAppEvaluationManager.m index 175ed4e8..f2896252 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.m +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.m @@ -23,6 +23,8 @@ @interface CTInAppEvaluationManager() @property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIds; @property (nonatomic, strong) NSMutableArray *suppressedClientSideInApps; +@property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIdsForProfile; +@property (nonatomic, strong) NSMutableArray *suppressedClientSideInAppsForProfile; @property BOOL hasAppLaunchedFailed; @property (nonatomic, strong) NSDictionary *appLaunchedProperties; @@ -36,8 +38,8 @@ @interface CTInAppEvaluationManager() @property (nonatomic, strong) NSString *accountId; @property (nonatomic, strong) NSString *deviceId; -- (void)evaluateServerSide:(CTEventAdapter *)event; -- (void)evaluateClientSide:(CTEventAdapter *)event; +- (void)evaluateServerSide:(NSArray *)events withQueueType:(CTQueueType)queueType; +- (void)evaluateClientSide:(NSArray *)events; - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApps; @end @@ -68,6 +70,18 @@ - (instancetype)initWithAccountId:(NSString *)accountId if (savedSuppressedClientSideInApps) { self.suppressedClientSideInApps = [savedSuppressedClientSideInApps mutableCopy]; } + + self.evaluatedServerSideInAppIdsForProfile = [NSMutableArray new]; + NSArray *savedEvaluatedServerSideInAppIdsForProfile = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE]]; + if (savedEvaluatedServerSideInAppIdsForProfile) { + self.evaluatedServerSideInAppIdsForProfile = [savedEvaluatedServerSideInAppIdsForProfile mutableCopy]; + } + + self.suppressedClientSideInAppsForProfile = [NSMutableArray new]; + NSArray *savedSuppressedClientSideInAppsForProfile = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE]]; + if (savedSuppressedClientSideInAppsForProfile) { + self.suppressedClientSideInAppsForProfile = [savedSuppressedClientSideInAppsForProfile mutableCopy]; + } self.inAppStore = inAppStore; self.triggersMatcher = [CTTriggersMatcher new]; @@ -87,19 +101,38 @@ - (void)evaluateOnEvent:(NSString *)eventName withProps:(NSDictionary *)properti } CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:eventName eventProperties:properties andLocation:self.location]; - [self evaluateServerSide:event]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + [self evaluateServerSide:eventList withQueueType:CTQueueTypeEvents]; + [self evaluateClientSide:eventList]; +} + +-(void)evaluateOnUserAttributeChange:(NSDictionary *)profile { + NSDictionary *appFields = self.appLaunchedProperties; + NSMutableArray *eventAdapterList = [NSMutableArray array]; + [profile enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + NSString *eventName = [key stringByAppendingString:CLTAP_USER_ATTRIBUTE_CHANGE]; + NSMutableDictionary *eventProperties = [NSMutableDictionary dictionaryWithDictionary:value]; + [eventProperties addEntriesFromDictionary:appFields]; + CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:eventName profileAttrName:key eventProperties: value andLocation:self.location]; + [eventAdapterList addObject:event]; + }]; + [self evaluateServerSide:eventAdapterList withQueueType:CTQueueTypeProfile]; + [self evaluateClientSide:eventAdapterList]; + } - (void)evaluateOnChargedEvent:(NSDictionary *)chargeDetails andItems:(NSArray *)items { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_CHARGED_EVENT eventProperties:chargeDetails location:self.location andItems:items]; - [self evaluateServerSide:event]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + CTQueueType queueType = CTQueueTypeEvents; + [self evaluateServerSide:eventList withQueueType:queueType]; + [self evaluateClientSide:eventList]; } - (void)evaluateOnAppLaunchedClientSide { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_APP_LAUNCHED_EVENT eventProperties:self.appLaunchedProperties andLocation:self.location]; - [self evaluateClientSide:event]; + NSArray *eventList = @[event]; + [self evaluateClientSide:eventList]; } - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs { @@ -116,8 +149,16 @@ - (void)evaluateOnAppLaunchedServerSide:(NSArray *)appLaunchedNotifs { } } -- (void)evaluateClientSide:(CTEventAdapter *)event { - NSMutableArray *eligibleInApps = [self evaluate:event withInApps:self.inAppStore.clientSideInApps]; +- (void)evaluateClientSide:(NSArray *)events { + NSMutableArray *eligibleInApps = [NSMutableArray array]; + for (CTEventAdapter *event in events) { + id oldValue = [event.eventProperties objectForKey:CLTAP_KEY_OLD_VALUE]; + id newValue = [event.eventProperties objectForKey:CLTAP_KEY_NEW_VALUE]; + if (event.profileAttrName != nil && newValue == oldValue) { + continue; + } + [eligibleInApps addObjectsFromArray:[self evaluate:event withInApps:self.inAppStore.clientSideInApps]]; + } [self sortByPriority:eligibleInApps]; for (NSDictionary *inApp in eligibleInApps) { @@ -132,8 +173,11 @@ - (void)evaluateClientSide:(CTEventAdapter *)event { } } -- (void)evaluateServerSide:(CTEventAdapter *)event { - NSArray *eligibleInApps = [self evaluate:event withInApps:self.inAppStore.serverSideInApps]; +- (void)evaluateServerSide:(NSArray *)events withQueueType:(CTQueueType)queueType{ + NSMutableArray *eligibleInApps = [NSMutableArray array]; + for (CTEventAdapter *event in events) { + [eligibleInApps addObjectsFromArray:[self evaluate:event withInApps:self.inAppStore.serverSideInApps]]; + } BOOL updated = NO; for (NSDictionary *inApp in eligibleInApps) { NSString *campaignId = [CTInAppNotification inAppId:inApp]; @@ -141,12 +185,22 @@ - (void)evaluateServerSide:(CTEventAdapter *)event { NSNumber *cid = [CTUtils numberFromString:campaignId]; if (cid) { updated = YES; - [self.evaluatedServerSideInAppIds addObject:cid]; + if (queueType == CTQueueTypeEvents){ + [self.evaluatedServerSideInAppIds addObject:cid]; + } + else if (queueType == CTQueueTypeProfile){ + [self.evaluatedServerSideInAppIdsForProfile addObject:cid]; + } } } } if (updated) { - [self saveEvaluatedServerSideInAppIds]; + if (queueType == CTQueueTypeEvents){ + [self saveEvaluatedServerSideInAppIds]; + } + else if (queueType == CTQueueTypeProfile){ + [self saveEvaluatedServerSideInAppIdsForProfile]; + } } } @@ -157,6 +211,10 @@ - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApp if (!campaignId) { continue; } + if (![self.inAppDisplayManager isTemplateRegistered:inApp]) { + continue; + } + // Match trigger NSArray *whenTriggers = inApp[CLTAP_INAPP_TRIGGERS]; BOOL matchesTrigger = [self.triggersMatcher matchEventWhenTriggers:whenTriggers event:event]; @@ -181,11 +239,17 @@ - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApp return eligibleInApps; } -- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success { +- (void)onBatchSent:(NSArray *)batchWithHeader withSuccess:(BOOL)success withQueueType:(CTQueueType)queueType{ if (success) { NSDictionary *header = batchWithHeader[0]; - [self removeSentEvaluatedServerSideInAppIds:header]; - [self removeSentSuppressedClientSideInApps:header]; + if (queueType == CTQueueTypeEvents) { + [self removeSentEvaluatedServerSideInAppIds:header]; + [self removeSentSuppressedClientSideInApps:header]; + } + else if (queueType == CTQueueTypeProfile) { + [self removeSentEvaluatedServerSideInAppIdsForProfile:header]; + [self removeSentSuppressedClientSideInAppsForProfile:header]; + } } } @@ -215,6 +279,25 @@ - (void)removeSentSuppressedClientSideInApps:(NSDictionary *)header { } } +- (void)removeSentEvaluatedServerSideInAppIdsForProfile:(NSDictionary *)header { + NSArray *inapps_eval = header[CLTAP_INAPP_SS_EVAL_META_KEY]; + if (inapps_eval && [inapps_eval count] > 0) { + NSUInteger len = inapps_eval.count > self.evaluatedServerSideInAppIdsForProfile.count ? self.evaluatedServerSideInAppIdsForProfile.count : inapps_eval.count; + [self.evaluatedServerSideInAppIdsForProfile removeObjectsInRange:NSMakeRange(0, len)]; + [self saveEvaluatedServerSideInAppIdsForProfile]; + } +} + +- (void)removeSentSuppressedClientSideInAppsForProfile:(NSDictionary *)header { + NSArray *suppresed_inapps = header[CLTAP_INAPP_SUPPRESSED_META_KEY]; + if (suppresed_inapps && [suppresed_inapps count] > 0) { + NSUInteger len = suppresed_inapps.count > self.suppressedClientSideInAppsForProfile.count ? self.suppressedClientSideInAppsForProfile.count : suppresed_inapps.count; + [self.suppressedClientSideInAppsForProfile removeObjectsInRange:NSMakeRange(0, len)]; + [self saveSuppressedClientSideInAppsForProfile]; + } +} + + - (BOOL)shouldSuppress:(NSDictionary *)inApp { return [inApp[CLTAP_INAPP_IS_SUPPRESSED] boolValue]; } @@ -298,18 +381,26 @@ - (void)updateTTL:(NSMutableDictionary *)inApp { - (BatchHeaderKeyPathValues)onBatchHeaderCreationForQueue:(CTQueueType)queueType { // Evaluation is done for events only at the moment, // send the evaluated and suppressed ids in that queue header - if (queueType != CTQueueTypeEvents) { + if (queueType != CTQueueTypeEvents && queueType != CTQueueTypeProfile) { return [NSMutableDictionary new]; } - NSMutableDictionary *header = [NSMutableDictionary new]; - if ([self.evaluatedServerSideInAppIds count] > 0) { - header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIds; + if (queueType == CTQueueTypeProfile) { + if ([self.evaluatedServerSideInAppIdsForProfile count] > 0) { + header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIdsForProfile; + } + if ([self.suppressedClientSideInAppsForProfile count] > 0) { + header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInAppsForProfile; + } } - if ([self.suppressedClientSideInApps count] > 0) { - header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInApps; + else { + if ([self.evaluatedServerSideInAppIds count] > 0) { + header[CLTAP_INAPP_SS_EVAL_META_KEY] = self.evaluatedServerSideInAppIds; + } + if ([self.suppressedClientSideInApps count] > 0) { + header[CLTAP_INAPP_SUPPRESSED_META_KEY] = self.suppressedClientSideInApps; + } } - return header; } @@ -321,6 +412,14 @@ - (void)saveSuppressedClientSideInApps { [CTPreferences putObject:self.suppressedClientSideInApps forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY]]; } +- (void)saveEvaluatedServerSideInAppIdsForProfile { + [CTPreferences putObject:self.evaluatedServerSideInAppIdsForProfile forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SS_EVAL_STORAGE_KEY_PROFILE]]; +} + +- (void)saveSuppressedClientSideInAppsForProfile { + [CTPreferences putObject:self.suppressedClientSideInAppsForProfile forKey:[self storageKeyWithSuffix:CLTAP_INAPP_SUPPRESSED_STORAGE_KEY_PROFILE]]; +} + - (NSString *)storageKeyWithSuffix:(NSString *)suffix { return [NSString stringWithFormat:@"%@:%@:%@", self.accountId, suffix, self.deviceId]; } diff --git a/CleverTapSDK/InApps/CTInAppHTMLViewController.h b/CleverTapSDK/InApps/CTInAppHTMLViewController.h index 38e3b61f..e1f29ff8 100644 --- a/CleverTapSDK/InApps/CTInAppHTMLViewController.h +++ b/CleverTapSDK/InApps/CTInAppHTMLViewController.h @@ -2,4 +2,6 @@ @interface CTInAppHTMLViewController : CTInAppDisplayViewController +- (instancetype)initWithNotification:(CTInAppNotification *)notification config:(CleverTapInstanceConfig *)config; + @end diff --git a/CleverTapSDK/InApps/CTInAppHTMLViewController.m b/CleverTapSDK/InApps/CTInAppHTMLViewController.m index 262b90de..a0d36943 100644 --- a/CleverTapSDK/InApps/CTInAppHTMLViewController.m +++ b/CleverTapSDK/InApps/CTInAppHTMLViewController.m @@ -1,7 +1,7 @@ #import #import "CTInAppHTMLViewController.h" #import "CTInAppDisplayViewControllerPrivate.h" -#import "CleverTapJSInterface.h" +#import "CleverTapJSInterfacePrivate.h" #import "CTUIUtils.h" #import "CTDismissButton.h" #import "CTUriHelper.h" @@ -44,10 +44,10 @@ @interface CTInAppHTMLViewController () webView.configuration.userContentController removeScriptMessageHandlerForName:@"clevertap"]; [self.window removeFromSuperview]; self.window = nil; if (self.delegate && [self.delegate respondsToSelector:@selector(notificationDidDismiss:fromViewController:)]) { diff --git a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h b/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h deleted file mode 100644 index 00a99be4..00000000 --- a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.h +++ /dev/null @@ -1,19 +0,0 @@ -#import -#import "CleverTapInstanceConfig.h" - -@class CTMultiDelegateManager; - -NS_ASSUME_NONNULL_BEGIN - -@interface CTInAppImagePrefetchManager : NSObject - -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; -- (void)preloadClientSideInAppImages:(NSArray *)csInAppNotifs; -- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL; -- (void)setImageAssetsInactiveAndClearExpired; -- (void)_clearImageAssets:(BOOL)expiredOnly; - -@end - -NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m b/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m deleted file mode 100644 index 8837866c..00000000 --- a/CleverTapSDK/InApps/CTInAppImagePrefetchManager.m +++ /dev/null @@ -1,254 +0,0 @@ -#import "CTConstants.h" -#import "CTPreferences.h" -#import "CTInAppImagePrefetchManager.h" -#import "CTMultiDelegateManager.h" -#import -#import - -static const NSInteger kDefaultInAppExpiryTime = 60 * 60 * 24 * 7 * 2; // 2 weeks - -@interface CTInAppImagePrefetchManager() - -@property (nonatomic, strong) CleverTapInstanceConfig *config; -@property (nonatomic, assign) SDWebImageOptions sdWebImageOptions; -@property (nonatomic, strong) SDWebImageContext *sdWebImageContext; -@property (nonatomic, strong) SDWebImageManager *sdWebImageManager; -@property (nonatomic, strong) SDImageCache *sdImageCache; -@property (nonatomic, strong) NSMutableSet *activeImageSet; -@property (nonatomic, strong) NSMutableSet *inactiveImageSet; -@property (nonatomic) NSTimeInterval inAppExpiryTime; - -@end - -@implementation CTInAppImagePrefetchManager - -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config { - self = [super init]; - if (self) { - self.config = config; - - [self setup]; - } - - return self; -} - -#pragma mark - Public - -- (void)preloadClientSideInAppImages:(NSArray *)csInAppNotifs { - if (csInAppNotifs.count == 0) return; - - NSArray *mediaURLs = [self getImageURLs:csInAppNotifs]; - [self prefetchURLs:mediaURLs]; -} - -- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL { - UIImage *image = [self.sdImageCache imageFromDiskCacheForKey:imageURL]; - if (image) return image; - - CleverTapLogInternal(self.config.logLevel, @"%@: Image not found in Disk Cache for URL: %@", self, imageURL); - return nil; -} - -- (void)setImageAssetsInactiveAndClearExpired { - // Move all active image url to inactive and store it in preference. - // Images will be deleted from Disk cache when expiration check is done. - // Check for expired images, if any delete them from disk cache. - [self moveAssetsToInactive]; - - if ([self.inactiveImageSet allObjects].count > 0) { - long lastDeletedTime = [self getLastDeletedTimestamp]; - [self removeInactiveExpiredAssets:lastDeletedTime]; - } -} - -- (void)_clearImageAssets:(BOOL)expiredOnly { - // When expiredOnly is true, delete inapp images from disk cache which are present in inactive set. - // When expiredOnly is false, delete all inapp images from disk cache for current user. - long lastDeletedTime = [self getLastDeletedTimestamp]; - if (!expiredOnly) { - [self moveAssetsToInactive]; - lastDeletedTime = ([self getLastDeletedTimestamp] - (kDefaultInAppExpiryTime + 1)); - } - - if ([self.inactiveImageSet allObjects].count > 0) { - [self removeInactiveExpiredAssets:lastDeletedTime]; - } -} - -#pragma mark - Private - -- (void)setup { - self.inAppExpiryTime = kDefaultInAppExpiryTime; - self.activeImageSet = [NSMutableSet new]; - self.inactiveImageSet = [NSMutableSet new]; - - [self addActiveImageAssets]; - [self addInactiveImageAssets]; - - self.sdWebImageOptions = (SDWebImageRetryFailed); - self.sdWebImageContext = @{SDWebImageContextStoreCacheType : @(SDImageCacheTypeDisk)}; - self.sdWebImageManager = [SDWebImageManager sharedManager]; - self.sdImageCache = [SDImageCache sharedImageCache]; - // Setting this to a negative value means no expiring. We will handle expired images at our side. - [[self.sdImageCache config] setMaxDiskAge:-1]; -} - -- (void)prefetchURLs:(NSArray *)mediaURLs { - if (mediaURLs.count == 0) return; - - // Download the images which are not present in Disk cache. - // Steps: - // 1. Check if new image url is present in inactiveImageSet - // 2. If present move image url to activeImageSet, else download and add it to activeImageSet - // 3. Check for expired images in inactiveImageSet when all images are downloaded - dispatch_group_t group = dispatch_group_create(); - dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - for (NSString *url in mediaURLs) { - // Check if image is present in disk cache or not. - // If present, add the url in `activeImageSet` and remove from `inactiveImageSet` if present. - UIImage *image = [self loadImageFromDisk:url]; - if (image) { - [self.activeImageSet addObject:url]; - if ([self.inactiveImageSet containsObject:url]) { - [self.inactiveImageSet removeObject:url]; - } - } else { - dispatch_group_enter(group); - dispatch_async(concurrentQueue, ^{ - [self.sdWebImageManager loadImageWithURL:[NSURL URLWithString:url] - options:self.sdWebImageOptions - context:self.sdWebImageContext - progress:nil - completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { - if (image) { - [self.activeImageSet addObject:imageURL.absoluteString]; - } - dispatch_group_leave(group); - }]; - }); - } - } - dispatch_group_notify(group, concurrentQueue, ^{ - // This block will be executed when all images are prefetched. - [self updateImageAssetsInPreference]; - long lastDeletedTime = [self getLastDeletedTimestamp]; - [self removeInactiveExpiredAssets:lastDeletedTime]; - }); -} - -- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { - // Remove the images which are in inactive asset set and over default expiration period. - // Steps: - // 1. Initially set the lastDeletedTime as current time(first time) - // 2. Check if lastDeletedTime + default expiry time has passed - // 3. Delete all images from inactive asset - // 4. Update lastDeletedTime as currentTime and image assets in preference - dispatch_group_t deleteGroup = dispatch_group_create(); - dispatch_queue_t deleteConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - if (lastDeletedTime > 0) { - long currentTime = (long) [[NSDate date] timeIntervalSince1970]; - if (currentTime - lastDeletedTime > self.inAppExpiryTime) { - // Delete all inactive expired images. - NSArray *inactiveAsset = [self.inactiveImageSet allObjects]; - for (NSString *url in inactiveAsset) { - dispatch_group_enter(deleteGroup); - dispatch_async(deleteConcurrentQueue, ^{ - [self.sdImageCache removeImageForKey:url - fromDisk:YES - withCompletion:^{ - [self.inactiveImageSet removeObject:url]; - dispatch_group_leave(deleteGroup); - }]; - }); - } - CleverTapLogInternal(self.config.logLevel, @"%@: Expired Images are removed from disk cache", self); - dispatch_group_notify(deleteGroup, deleteConcurrentQueue, ^{ - // This block will be executed when all images are removed. - if ([self.inactiveImageSet allObjects].count == 0) { - // Move all images to inactive for the new expiration window - [self moveAssetsToInactive]; - // Update last deleted time only when all inactive images are deleted - [self updateLastDeletedTimestamp]; - } - }); - } - } -} - -- (NSArray *)getImageURLs:(NSArray *)csInAppNotifs { - NSMutableSet *mediaURLs = [NSMutableSet new]; - for (NSDictionary *jsonInApp in csInAppNotifs) { - NSDictionary *media = (NSDictionary*) jsonInApp[@"media"]; - if (media) { - NSString *imageURL = [self getURLFromDictionary:media]; - if (imageURL) { - [mediaURLs addObject:imageURL]; - } - } - NSDictionary *mediaLandscape = (NSDictionary*) jsonInApp[@"mediaLandscape"]; - if (mediaLandscape) { - NSString *imageURL = [self getURLFromDictionary:mediaLandscape]; - if (imageURL) { - [mediaURLs addObject:imageURL]; - } - } - } - return [mediaURLs allObjects]; -} - -- (NSString *)getURLFromDictionary:(NSDictionary *)media { - NSString *contentType = media[@"content_type"]; - NSString *mediaUrl = media[@"url"]; - if (mediaUrl && mediaUrl.length > 0) { - // Preload contentType with image/jpeg or image/gif - if ([contentType hasPrefix:@"image"]) { - return mediaUrl; - } - } - return nil; -} - -- (NSString *)storageKeyWithSuffix:(NSString *)suffix { - return [NSString stringWithFormat:@"%@:%@", self.config.accountId, suffix]; -} - -- (long)getLastDeletedTimestamp { - long now = (long) [[NSDate date] timeIntervalSince1970]; - long lastDeletedTime = [CTPreferences getIntForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS] - withResetValue:now]; - return lastDeletedTime; -} - -- (void)updateLastDeletedTimestamp { - long now = (long) [[NSDate date] timeIntervalSince1970]; - [CTPreferences putInt:now - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; -} - -- (void)moveAssetsToInactive { - [self.inactiveImageSet unionSet:self.activeImageSet]; - self.activeImageSet = [NSMutableSet new]; - [self updateImageAssetsInPreference]; -} - -- (void)addInactiveImageAssets { - // Add only inactive array from preferences in `inactiveImageSet` - NSArray *inactiveAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; - [self.inactiveImageSet addObjectsFromArray:inactiveAssetsArray]; -} - -- (void)addActiveImageAssets { - // Add only active array from preferences in `activeImageSet` - NSArray *activeAssetsArray = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; - [self.activeImageSet addObjectsFromArray:activeAssetsArray]; -} - -- (void)updateImageAssetsInPreference { - [CTPreferences putObject:[self.activeImageSet allObjects] - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; - [CTPreferences putObject:[self.inactiveImageSet allObjects] - forKey:[self storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; -} - -@end diff --git a/CleverTapSDK/InApps/CTInAppStore.h b/CleverTapSDK/InApps/CTInAppStore.h index d5517554..2c186569 100644 --- a/CleverTapSDK/InApps/CTInAppStore.h +++ b/CleverTapSDK/InApps/CTInAppStore.h @@ -8,7 +8,6 @@ #import #import "CTSwitchUserDelegate.h" -#import "CTInAppImagePrefetchManager.h" @class CleverTapInstanceConfig; @class CTMultiDelegateManager; @@ -20,7 +19,6 @@ - (instancetype _Nonnull)init NS_UNAVAILABLE; - (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig * _Nonnull)config delegateManager:(CTMultiDelegateManager * _Nonnull)delegateManager - imagePrefetchManager:(CTInAppImagePrefetchManager * _Nonnull)imagePrefetchManager deviceId:(NSString * _Nonnull)deviceId; - (NSArray * _Nonnull)clientSideInApps; @@ -33,6 +31,7 @@ - (NSArray * _Nonnull)inAppsQueue; - (void)storeInApps:(NSArray * _Nullable)inApps; - (void)enqueueInApps:(NSArray * _Nullable)inAppNotifs; +- (void)insertInFrontInApp:(NSDictionary * _Nullable)inAppNotif; - (NSDictionary * _Nullable)peekInApp; - (NSDictionary * _Nullable)dequeueInApp; diff --git a/CleverTapSDK/InApps/CTInAppStore.m b/CleverTapSDK/InApps/CTInAppStore.m index 30da70ff..46877a34 100644 --- a/CleverTapSDK/InApps/CTInAppStore.m +++ b/CleverTapSDK/InApps/CTInAppStore.m @@ -12,7 +12,6 @@ #import "CTAES.h" #import "CleverTapInstanceConfig.h" #import "CleverTapInstanceConfigPrivate.h" -#import "CTInAppImagePrefetchManager.h" #import "CTMultiDelegateManager.h" NSString* const kCLIENT_SIDE_MODE = @"CS"; @@ -23,7 +22,6 @@ @interface CTInAppStore() @property (nonatomic, strong) CleverTapInstanceConfig *config; @property (nonatomic, strong) NSString *accountId; @property (nonatomic, strong) NSString *deviceId; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; @property (nonatomic, strong) CTAES *ctAES; @property (nonatomic, strong) NSArray *inAppsQueue; @@ -38,7 +36,6 @@ @implementation CTInAppStore - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config delegateManager:(CTMultiDelegateManager *)delegateManager - imagePrefetchManager:(CTInAppImagePrefetchManager *)imagePrefetchManager deviceId:(NSString *)deviceId { self = [super init]; if (self) { @@ -46,7 +43,6 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config self.accountId = config.accountId; self.deviceId = deviceId; self.ctAES = [[CTAES alloc] initWithAccountID:config.accountId]; - self.imagePrefetchManager = imagePrefetchManager; [delegateManager addSwitchUserDelegate:self]; [self migrateInAppQueueKeys]; @@ -131,6 +127,16 @@ - (void)enqueueInApps:(NSArray *)inAppNotifs { } } +- (void)insertInFrontInApp:(NSDictionary *)inAppNotif { + if (!inAppNotif) return; + + @synchronized(self) { + NSMutableArray *inAppsQueue = [[NSMutableArray alloc] initWithArray:[self inAppsQueue]]; + [inAppsQueue insertObject:inAppNotif atIndex:0]; + [self storeInApps:inAppsQueue]; + } +} + - (NSDictionary *)peekInApp { @synchronized(self) { NSArray *inApps = [self inAppsQueue]; @@ -180,9 +186,6 @@ - (void)setMode:(nullable NSString *)mode { #pragma mark Client-Side In-Apps - (void)removeClientSideInApps { @synchronized (self) { - // Clear the CS images stored in disk cache - [self.imagePrefetchManager setImageAssetsInactiveAndClearExpired]; - _clientSideInApps = [NSArray new]; NSString *storageKey = [self storageKeyWithSuffix:CLTAP_PREFS_INAPP_KEY_CS]; [CTPreferences removeObjectForKey:storageKey]; @@ -195,9 +198,6 @@ - (void)storeClientSideInApps:(NSArray *)clientSideInApps { @synchronized (self) { _clientSideInApps = clientSideInApps; - // Preload CS inApp images to disk cache - [self.imagePrefetchManager preloadClientSideInAppImages:_clientSideInApps]; - NSString *encryptedString = [self.ctAES getEncryptedBase64String:clientSideInApps]; NSString *storageKey = [self storageKeyWithSuffix:CLTAP_PREFS_INAPP_KEY_CS]; [CTPreferences putString:encryptedString forKey:storageKey]; diff --git a/CleverTapSDK/InApps/CTInterstitialViewController.m b/CleverTapSDK/InApps/CTInterstitialViewController.m index c9acc89d..95e28f9a 100644 --- a/CleverTapSDK/InApps/CTInterstitialViewController.m +++ b/CleverTapSDK/InApps/CTInterstitialViewController.m @@ -145,13 +145,13 @@ - (void)layoutNotification { if (self.notification.inAppImage) { self.imageView.contentMode = UIViewContentModeScaleAspectFit; self.imageView.image = self.notification.inAppImage; - } else if (self.notification.image) { + } else if (self.notification.imageData) { self.imageView.contentMode = UIViewContentModeScaleAspectFit; if ([self.notification.contentType isEqualToString:@"image/gif"] ) { - SDAnimatedImage *gif = [SDAnimatedImage imageWithData:self.notification.image]; + SDAnimatedImage *gif = [SDAnimatedImage imageWithData:self.notification.imageData]; self.imageView.image = gif; } else { - self.imageView.image = [UIImage imageWithData:self.notification.image]; + self.imageView.image = [UIImage imageWithData:self.notification.imageData]; } } diff --git a/CleverTapSDK/InApps/CTLocalInApp.m b/CleverTapSDK/InApps/CTLocalInApp.m index 47312c00..3240efa3 100644 --- a/CleverTapSDK/InApps/CTLocalInApp.m +++ b/CleverTapSDK/InApps/CTLocalInApp.m @@ -1,5 +1,6 @@ #import #import "CTLocalInApp.h" +#import "CTConstants.h" @interface CTLocalInApp () {} @property (nonatomic, strong) NSString *inAppType; @@ -113,11 +114,11 @@ - (void)setBtnBackgroundColor:(NSString *)btnBackgroundColor { - (void)setImageUrl:(NSString *)imageUrl { NSMutableDictionary *mediaObj = [NSMutableDictionary new]; - mediaObj[@"content_type"] = @"image"; - mediaObj[@"url"] = imageUrl; - self.inAppSettings[@"media"] = mediaObj; + mediaObj[CLTAP_INAPP_MEDIA_CONTENT_TYPE] = @"image"; + mediaObj[CLTAP_INAPP_MEDIA_URL] = imageUrl; + self.inAppSettings[CLTAP_INAPP_MEDIA] = mediaObj; if (self.followDeviceOrientation) { - self.inAppSettings[@"mediaLandscape"] = mediaObj; + self.inAppSettings[CLTAP_INAPP_MEDIA_LANDSCAPE] = mediaObj; } } diff --git a/CleverTapSDK/InApps/CleverTap+InAppsResponseHandler.m b/CleverTapSDK/InApps/CleverTap+InAppsResponseHandler.m index 3a8b7a9d..ed6caa41 100644 --- a/CleverTapSDK/InApps/CleverTap+InAppsResponseHandler.m +++ b/CleverTapSDK/InApps/CleverTap+InAppsResponseHandler.m @@ -14,6 +14,7 @@ #import "CTConstants.h" #import "CleverTapInternal.h" #import "CTUtils.h" +#import "CTCustomTemplatesManager-Internal.h" @implementation CleverTap(InAppsResponseHandler) @@ -44,6 +45,11 @@ - (void)handleInAppResponse:(NSDictionary *)jsonResp { NSArray *csInAppNotifs = jsonResp[CLTAP_INAPP_CS_JSON_RESPONSE_KEY]; if (csInAppNotifs) { [self.inAppStore storeClientSideInApps:csInAppNotifs]; + + // Preload CS in-app images to disk cache + [self downloadMediaURLs:csInAppNotifs]; + // Preload CS custom template in-app files to disk cache + [self downloadCustomTemplatesFileURLs:csInAppNotifs]; } // Parse in-app Mode @@ -93,6 +99,53 @@ - (void)handleInAppResponse:(NSDictionary *)jsonResp { [self triggerFetchInApps:YES]; } +- (void)downloadMediaURLs:(NSArray *)inApps { + NSArray *imageURLs = [self imageURLs:inApps]; + [self.fileDownloader downloadFiles:imageURLs withCompletionBlock:nil]; +} + +- (void)downloadCustomTemplatesFileURLs:(NSArray *)inApps { + NSMutableSet *urls = [NSMutableSet set]; + for (NSDictionary *inApp in inApps) { + NSSet *inAppFileArgsUrls = [self.customTemplatesManager fileArgsURLs:inApp]; + [urls unionSet:inAppFileArgsUrls]; + } + [self.fileDownloader downloadFiles:[urls allObjects] withCompletionBlock:nil]; +} + +- (NSArray *)imageURLs:(NSArray *)inApps { + NSMutableSet *mediaURLs = [NSMutableSet new]; + for (NSDictionary *jsonInApp in inApps) { + NSDictionary *media = (NSDictionary *)jsonInApp[CLTAP_INAPP_MEDIA]; + if (media) { + NSString *imageURL = [self URLFromMedia:media]; + if (imageURL) { + [mediaURLs addObject:imageURL]; + } + } + NSDictionary *mediaLandscape = (NSDictionary *)jsonInApp[CLTAP_INAPP_MEDIA_LANDSCAPE]; + if (mediaLandscape) { + NSString *imageURL = [self URLFromMedia:mediaLandscape]; + if (imageURL) { + [mediaURLs addObject:imageURL]; + } + } + } + return [mediaURLs allObjects]; +} + +- (NSString *)URLFromMedia:(NSDictionary *)media { + NSString *contentType = media[CLTAP_INAPP_MEDIA_CONTENT_TYPE]; + NSString *mediaUrl = media[CLTAP_INAPP_MEDIA_URL]; + if (mediaUrl && mediaUrl.length > 0) { + // Preload contentType with image/jpeg or image/gif + if ([contentType hasPrefix:@"image"]) { + return mediaUrl; + } + } + return nil; +} + - (void)triggerFetchInApps:(BOOL)success { if (self.fetchInAppsBlock) { CleverTapFetchInAppsBlock block = [self.fetchInAppsBlock copy]; diff --git a/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.h b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.h new file mode 100644 index 00000000..ee5647a8 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.h @@ -0,0 +1,20 @@ +// +// CTAppFunctionBuilder.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTCustomTemplateBuilder.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTAppFunctionBuilder : CTCustomTemplateBuilder + +- (instancetype)initWithIsVisual:(BOOL)isVisual; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m new file mode 100644 index 00000000..5fe962ce --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m @@ -0,0 +1,21 @@ +// +// CTAppFunctionBuilder.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTAppFunctionBuilder.h" +#import "CTTemplateArgument.h" +#import "CTCustomTemplateBuilder-Internal.h" +#import "CTCustomTemplate-Internal.h" + +@implementation CTAppFunctionBuilder + +- (nonnull instancetype)initWithIsVisual:(BOOL)isVisual { + self = [super initWithType:FUNCTION_TYPE isVisual:isVisual]; + return self; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h new file mode 100644 index 00000000..0cee8cf6 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h @@ -0,0 +1,30 @@ +// +// CTCustomTemplate-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 28.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTCustomTemplate_Internal_h +#define CTCustomTemplate_Internal_h + +#import "CTTemplatePresenter.h" +#import "CTCustomTemplate.h" +#import "CTTemplateArgument.h" + +@interface CTCustomTemplate (Internal) + +@property (nonatomic, strong, readonly) NSString *templateType; +@property (nonatomic, strong, readonly) NSArray *arguments; +@property (nonatomic, strong, readonly) id presenter; + +- (instancetype)initWithTemplateName:(NSString *)templateName + templateType:(NSString *)templateType + isVisual:(BOOL)isVisual + arguments:(NSArray *)arguments + presenter:(id)presenter; + +@end + +#endif /* CTCustomTemplate_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h new file mode 100644 index 00000000..2fd46e65 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h @@ -0,0 +1,22 @@ +// +// CTCustomTemplate.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CTCustomTemplate : NSObject + +@property (nonatomic, strong, readonly) NSString *name; +@property (nonatomic, readonly) BOOL isVisual; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m new file mode 100644 index 00000000..c2bff9d0 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m @@ -0,0 +1,64 @@ +// +// CTCustomTemplate.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTCustomTemplate.h" +#import "CTCustomTemplate-Internal.h" + +@interface CTCustomTemplate () + +@property (nonatomic, strong) NSString *name; +@property (nonatomic) BOOL isVisual; +@property (nonatomic, strong) NSString *templateType; +@property (nonatomic, strong) NSArray *arguments; +@property (nonatomic, strong) id presenter; + +@end + +@implementation CTCustomTemplate + +- (instancetype)initWithTemplateName:(NSString *)templateName + templateType:(NSString *)templateType + isVisual:(BOOL)isVisual + arguments:(NSArray *)arguments + presenter:(id)presenter { + if (self = [super init]) { + _name = [templateName copy]; + _templateType = [templateType copy]; + _isVisual = isVisual; + _arguments = arguments; + _presenter = presenter; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[CTCustomTemplate class]]) { + return NO; + } + CTCustomTemplate *otherTemplate = (CTCustomTemplate *)other; + return [self.name isEqualToString:otherTemplate.name]; +} + +- (NSUInteger)hash { + return [self.name hash]; +} + +- (NSString *)debugDescription { + return [NSString stringWithFormat:@"<%@: %p> name: %@, isVisual: %@, type: %@, args: %@", + [self class], + self, + self.name, + self.isVisual ? @"YES" : @"NO", + self.templateType, + self.arguments.count > 0 ? [NSString stringWithFormat:@"{\n%@\n}", [self.arguments componentsJoinedByString:@",\n"]] : @"{\n}"]; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder-Internal.h new file mode 100644 index 00000000..8965c9d5 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder-Internal.h @@ -0,0 +1,38 @@ +// +// CTCustomTemplateBuilder-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 6.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// +#import "CTCustomTemplateBuilder.h" +#import "CTTemplateArgument.h" + +#ifndef CTCustomTemplateBuilder_Internal_h +#define CTCustomTemplateBuilder_Internal_h + +NS_ASSUME_NONNULL_BEGIN + +@interface CTCustomTemplateBuilder () + +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *templateType; +@property (nonatomic, assign) BOOL isVisual; + +@property (nonatomic, strong) NSSet *nullableArgumentTypes; +@property (nonatomic, strong) NSMutableSet *argumentNames; +@property (nonatomic, strong) NSMutableSet *parentArgumentNames; +@property (nonatomic, strong) NSMutableArray *arguments; + +@property (nonatomic, strong) id presenter; + +- (instancetype)initWithType:(NSString *)type isVisual:(BOOL)isVisual; +- (instancetype)initWithType:(NSString *)type isVisual:(BOOL)isVisual nullableArgumentTypes:(NSSet *)nullableArgumentTypes; + +- (void)addArgumentWithName:(NSString *)name type:(CTTemplateArgumentType)type defaultValue:(nullable id)defaultValue; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* CTCustomTemplateBuilder_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h new file mode 100644 index 00000000..6a9df22b --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h @@ -0,0 +1,44 @@ +// +// CTCustomTemplateBuilder.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 6.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTTemplatePresenter.h" +#import "CTCustomTemplate.h" + +#define TEMPLATE_TYPE @"template" +#define FUNCTION_TYPE @"function" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTCustomTemplateBuilder : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (void)setName:(NSString *)name; + +- (void)addArgument:(NSString *)name withString:(NSString *)defaultValue +NS_SWIFT_NAME(addArgument(_:string:)); + +- (void)addArgument:(NSString *)name withNumber:(NSNumber *)defaultValue +NS_SWIFT_NAME(addArgument(_:number:)); + +- (void)addArgument:(NSString *)name withBool:(BOOL)defaultValue +NS_SWIFT_NAME(addArgument(_:boolean:)); + +- (void)addArgument:(nonnull NSString *)name withDictionary:(nonnull NSDictionary *)defaultValue +NS_SWIFT_NAME(addArgument(_:dictionary:)); + +- (void)addFileArgument:(NSString *)name; + +- (void)setPresenter:(id)presenter; + +- (CTCustomTemplate *)build; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m new file mode 100644 index 00000000..d7a8b19c --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m @@ -0,0 +1,188 @@ +// +// CTCustomTemplateBuilder.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 6.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTCustomTemplateBuilder.h" +#import "CTCustomTemplateBuilder-Internal.h" +#import "CTCustomTemplate-Internal.h" +#import "CTTemplatePresenter.h" + +@implementation CTCustomTemplateBuilder + +- (instancetype)initWithType:(nonnull NSString *)type isVisual:(BOOL)isVisual { + return [self initWithType:type isVisual:isVisual nullableArgumentTypes:[NSSet setWithObject:@(CTTemplateArgumentTypeFile)]]; +} + +- (instancetype)initWithType:(nonnull NSString *)type isVisual:(BOOL)isVisual nullableArgumentTypes:(NSSet *)nullableArgumentTypes { + self = [super init]; + if (self) { + _templateType = [type copy]; + _isVisual = isVisual; + + _nullableArgumentTypes = [NSSet setWithObject:@(CTTemplateArgumentTypeFile)]; + if (nullableArgumentTypes && [nullableArgumentTypes count] > 0) { + _nullableArgumentTypes = [_nullableArgumentTypes setByAddingObjectsFromSet:nullableArgumentTypes]; + } + _argumentNames = [NSMutableSet set]; + _parentArgumentNames = [NSMutableSet set]; + _arguments = [NSMutableArray array]; + } + return self; +} + +- (void)addArgumentWithName:(NSString *)name type:(CTTemplateArgumentType)type defaultValue:(id)defaultValue { + NSAssert(![defaultValue isKindOfClass:[NSDictionary class]], @"Argument 'defaultValue' cannot be of type NSDictionary."); + + if (!name || [name isEqualToString:@""]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:@"CleverTap: Argument Name cannot be null or empty." + userInfo:nil]); + } + + if ([name hasPrefix:@"."] || [name hasSuffix:@"."] || [name containsString:@".."]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:@"CleverTap: Argument Name cannot start or end with dot \".\" or contain consecutive dots \"..\"." + userInfo:nil]); + } + + if ([self.argumentNames containsObject:name]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Argument with the name \"%@\" already defined.", name] + userInfo:nil]); + } + + if (![self.nullableArgumentTypes containsObject:@(type)] && + (defaultValue == nil || [defaultValue isEqual:[NSNull null]])) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Default value cannot be nil."] + userInfo:nil]); + } + + [self validateParentNames:name]; + + CTTemplateArgument *arg = [[CTTemplateArgument alloc] initWithName:name type:type defaultValue:defaultValue]; + [self.arguments addObject:arg]; + [self.argumentNames addObject:name]; +} + +- (void)validateParentNames:(NSString *)name { + NSArray *nameComponents = [name componentsSeparatedByString:@"."]; + + NSMutableString *parentName = [NSMutableString string]; + for (int i = 0; i < nameComponents.count - 1; i++) { + if (i > 0) { + [parentName appendString:@"."]; + } + NSString *component = nameComponents[i]; + [parentName appendString:component]; + + if ([self.argumentNames containsObject:parentName]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Argument with the name \"%@\" already defined.", parentName] + userInfo:nil]); + } + [self.parentArgumentNames addObject:[parentName copy]]; + } + + if ([self.parentArgumentNames containsObject:name]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Argument with the name \"%@\" already defined.", name] + userInfo:nil]); + } +} + +- (void)addArgument:(NSString *)name withBool:(BOOL)defaultValue { + [self addArgumentWithName:name type:CTTemplateArgumentTypeBool defaultValue:[NSNumber numberWithBool:defaultValue]]; +} + +- (void)addArgument:(NSString *)name withString:(NSString *)defaultValue { + [self addArgumentWithName:name type:CTTemplateArgumentTypeString defaultValue:defaultValue]; +} + +- (void)addArgument:(NSString *)name withNumber:(NSNumber *)defaultValue { + [self addArgumentWithName:name type:CTTemplateArgumentTypeNumber defaultValue:defaultValue]; +} + +- (void)addArgument:(nonnull NSString *)name withDictionary:(nonnull NSDictionary *)defaultValue { + if (defaultValue == nil || [defaultValue count] == 0) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Default value cannot be nil or empty."] + userInfo:nil]); + } + + [self flatten:defaultValue name:name]; +} + +- (void)flatten:(NSDictionary *)map name:(NSString *)name { + [map enumerateKeysAndObjectsUsingBlock:^(NSString* _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { + NSString *argName = [NSString stringWithFormat:@"%@.%@", name, key]; + if ([value isKindOfClass:[NSString class]]) { + [self addArgument:argName withString:value]; + } else if ([value isKindOfClass:[NSNumber class]]) { + [self addArgument:argName withNumber:value]; + } else if ([value isKindOfClass:[NSDictionary class]]) { + [self flatten:value name:argName]; + } + }]; +} + +- (void)addFileArgument:(NSString *)name { + [self addArgumentWithName:name type:CTTemplateArgumentTypeFile defaultValue:nil]; +} + +- (void)setName:(NSString *)name { + if (_name) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:@"CleverTap: Name is already set." + userInfo:nil]); + } + + if (name == nil || [name isEqualToString:@""]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:@"CleverTap: Name cannot be null or empty." + userInfo:nil]); + } + + _name = name; +} + +- (void)setPresenter:(id)presenter { + _presenter = presenter; +} + +- (CTCustomTemplate *)build { + if (!self.name || [self.name isEqualToString:@""]) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Name cannot be null or empty. Use setName to set it."] + userInfo:nil]); + } + + if (!self.presenter) { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Presenter cannot be null. Use setOnPresentWithPresenter to set it."] + userInfo:nil]); + } + + return [[CTCustomTemplate alloc] initWithTemplateName:self.name + templateType:self.templateType + isVisual:self.isVisual + arguments:self.arguments + presenter:self.presenter]; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h new file mode 100644 index 00000000..7bc538bd --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h @@ -0,0 +1,20 @@ +// +// CTCustomTemplateInAppData-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 6.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTCustomTemplateInAppData_Internal_h +#define CTCustomTemplateInAppData_Internal_h + +#import "CTCustomTemplateInAppData.h" + +@interface CTCustomTemplateInAppData (Internal) + +@property (nonatomic, readwrite) BOOL isAction; + +@end + +#endif /* CTCustomTemplateInAppData_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h new file mode 100644 index 00000000..ebd40d1d --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h @@ -0,0 +1,29 @@ +// +// CTCustomTemplateInAppData.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 9.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CTCustomTemplateInAppData : NSObject + +@property (nonatomic, copy, readonly) NSString *templateName; +@property (nonatomic, copy, readonly) NSString *templateId; +@property (nonatomic, copy, readonly) NSString *templateDescription; +@property (nonatomic, strong, readonly) NSDictionary *args; +@property (nonatomic, readonly) BOOL isAction; +@property (nonatomic, strong, readonly) NSDictionary *json; + +- (instancetype)init NS_UNAVAILABLE; +#if !CLEVERTAP_NO_INAPP_SUPPORT ++ (instancetype _Nullable)createWithJSON:(NSDictionary * _Nonnull)json; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m new file mode 100644 index 00000000..f1e2bd70 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m @@ -0,0 +1,78 @@ +// +// CTCustomTemplateInAppData.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 9.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTCustomTemplateInAppData.h" +#import "CTConstants.h" +#import "CTInAppUtils.h" + +@interface CTCustomTemplateInAppData() + +@property (nonatomic, copy, readwrite) NSString *templateName; +@property (nonatomic, copy, readwrite) NSString *templateId; +@property (nonatomic, copy, readwrite) NSString *templateDescription; +@property (nonatomic, strong, readwrite) NSDictionary *args; + +@property (nonatomic, strong, readwrite) NSDictionary *json; + +@end + +@implementation CTCustomTemplateInAppData + +- (nonnull instancetype)initWithJSON:(nonnull NSDictionary *)json { + if (self = [super init]) { + @try { + self.json = json; + self.templateName = json[CLTAP_INAPP_TEMPLATE_NAME]; + self.templateId = json[CLTAP_INAPP_TEMPLATE_ID]; + self.templateDescription = json[CLTAP_INAPP_TEMPLATE_DESCRIPTION]; + id isAction = json[@"is_action"]; + + if (isAction && [isAction isKindOfClass:[NSNumber class]]) { + self.isAction = [isAction boolValue]; + } + + id vars = json[CLTAP_INAPP_VARS]; + if ([vars isKindOfClass:[NSDictionary class]]) { + self.args = vars; + } + } @catch (NSException *e) { + CleverTapLogStaticInfo(@"Cannot initialize %@ with json:%@. Error: %@.", self.class, json, [e debugDescription]); + } + } + return self; +} + ++ (instancetype)createWithJSON:(nonnull NSDictionary *)json { + NSString *inAppType = json[CLTAP_INAPP_TYPE]; + if ([CTInAppUtils inAppTypeFromString:inAppType] == CTInAppTypeCustom) { + return [[CTCustomTemplateInAppData alloc] initWithJSON:json]; + } + return nil; +} + +- (void)setIsAction:(BOOL)isAction { + _isAction = isAction; + NSMutableDictionary *jsonMutable = [self.json mutableCopy]; + jsonMutable[@"is_action"] = @(isAction); + self.json = jsonMutable; +} + +- (id)copyWithZone:(NSZone *)zone { + CTCustomTemplateInAppData *copy = [[[self class] allocWithZone:zone] init]; + if (copy) { + copy->_templateName = [_templateName copyWithZone:zone]; + copy->_templateId = [_templateId copyWithZone:zone]; + copy->_templateDescription = [_templateDescription copyWithZone:zone]; + copy->_args = [[NSDictionary allocWithZone:zone] initWithDictionary:self.args copyItems:YES]; + copy->_json = [[NSDictionary allocWithZone:zone] initWithDictionary:self.json copyItems:YES]; + copy->_isAction = _isAction; + } + return copy; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h new file mode 100644 index 00000000..2e8c0d82 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h @@ -0,0 +1,33 @@ +// +// CTCustomTemplatesManager-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 21.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTCustomTemplatesManager_Internal_h +#define CTCustomTemplatesManager_Internal_h + +#import "CTCustomTemplatesManager.h" +#import "CleverTapInstanceConfig.h" +#import "CTInAppNotification.h" +#import "CTInAppNotificationDisplayDelegate.h" +#import "CTFileDownloader.h" + +@interface CTCustomTemplatesManager (Internal) + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)instanceConfig; + +- (NSSet *)fileArgsURLsForInAppData:(CTCustomTemplateInAppData *)inAppData; +- (NSSet *)fileArgsURLs:(NSDictionary *)inAppJSON; + +- (BOOL)presentNotification:(CTInAppNotification *)notification + withDelegate:(id)delegate + andFileDownloader:(CTFileDownloader *)fileDownloader; + +- (void)closeNotification:(CTInAppNotification *)notification; + +@end + +#endif /* CTCustomTemplatesManager_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h new file mode 100644 index 00000000..9334a030 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h @@ -0,0 +1,29 @@ +// +// CTCustomTemplatesManager.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 28.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTTemplateProducer.h" +#import "CTTemplateContext.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTCustomTemplatesManager : NSObject + ++ (void)registerTemplateProducer:(id)producer; + +- (instancetype)init NS_UNAVAILABLE; + +- (BOOL)isRegisteredTemplateWithName:(NSString *)name; +- (BOOL)isVisualTemplateWithName:(nonnull NSString *)name; +- (CTTemplateContext *)activeContextForTemplate:(NSString *)templateName; + +- (NSDictionary*)syncPayload; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m new file mode 100644 index 00000000..0dabf21c --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m @@ -0,0 +1,240 @@ +// +// CTCustomTemplatesManager.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 28.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTCustomTemplatesManager.h" +#import "CTCustomTemplate-Internal.h" +#import "CTTemplateArgument.h" +#import "CTConstants.h" +#import "CTInAppNotification.h" +#import "CTTemplateContext-Internal.h" + +@interface CTCustomTemplatesManager () + +@property (nonatomic, strong) NSMutableDictionary *templates; +@property (nonatomic, strong) NSMutableDictionary *activeContexts; + +@end + +@implementation CTCustomTemplatesManager + +static NSMutableArray> *templateProducers; + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + templateProducers = [NSMutableArray array]; + }); +} + ++ (void)registerTemplateProducer:(nonnull id)producer { + [templateProducers addObject:producer]; +} + ++ (void)clearTemplateProducers { + [templateProducers removeAllObjects]; +} + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)instanceConfig { + self = [super init]; + if (self) { + self.activeContexts = [NSMutableDictionary dictionary]; + self.templates = [NSMutableDictionary dictionary]; + for (id producer in templateProducers) { + NSSet *customTemplates = [producer defineTemplates:instanceConfig]; + for (CTCustomTemplate *template in customTemplates) { + if (!self.templates[template.name]) { + self.templates[template.name] = template; + } else { + @throw([NSException + exceptionWithName:@"CleverTap Error" + reason:[NSString stringWithFormat:@"CleverTap: Template with name: %@ is already defined.", template.name] + userInfo:nil]); + } + } + } + } + return self; +} + +- (BOOL)isRegisteredTemplateWithName:(nonnull NSString *)name { + return self.templates[name]; +} + +- (BOOL)isVisualTemplateWithName:(nonnull NSString *)name { + return self.templates[name].isVisual; +} + +- (CTTemplateContext *)activeContextForTemplate:(NSString *)templateName { + return self.activeContexts[templateName]; +} + +- (void)onDismissContext:(CTTemplateContext *)context { + [self.activeContexts removeObjectForKey:context.templateName]; +} + +- (BOOL)presentNotification:(CTInAppNotification *)notification + withDelegate:(id)delegate + andFileDownloader:(CTFileDownloader *)fileDownloader { + CTCustomTemplate *template = self.templates[notification.customTemplateInAppData.templateName]; + if (!template) { + CleverTapLogStaticDebug("%@: Template with name: %@ not registered.", self, notification.customTemplateInAppData.templateName); + return NO; + } + + CTTemplateContext *context = [self createTemplateContext:template + withNotification:notification + delegate:delegate + andFileDownloader:fileDownloader]; + self.activeContexts[template.name] = context; + [template.presenter onPresent:context]; + return YES; +} + +- (CTTemplateContext *)createTemplateContext:(CTCustomTemplate *)template + withNotification:(CTInAppNotification *)notification + delegate:(id)delegate + andFileDownloader:(CTFileDownloader *)fileDownloader { + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:template notification:notification andFileDownloader:fileDownloader]; + [context setNotificationDelegate:delegate]; + [context setDismissDelegate:self]; + return context; +} + +- (void)closeNotification:(CTInAppNotification *)notification { + NSString *templateName = notification.customTemplateInAppData.templateName; + if (!templateName) { + CleverTapLogStaticDebug("%@: No template name set in the notification template data.", [self class]); + return; + } + + CTCustomTemplate *template = self.templates[templateName]; + if (!template) { + CleverTapLogStaticDebug("%@: Template with name: %@ not registered.", [self class], templateName); + return; + } + + CTTemplateContext *context = [self activeContextForTemplate:templateName]; + if (!context) { + CleverTapLogStaticDebug("%@: Cannot find active context for template: %@.", [self class], templateName); + return; + } + + if (template.presenter) { + [template.presenter onCloseClicked:context]; + } +} + +- (NSSet *)fileArgsURLsForInAppData:(CTCustomTemplateInAppData *)inAppData { + NSMutableSet *urls = [NSMutableSet set]; + if (!inAppData) { + return urls; + } + + CTCustomTemplate *template = self.templates[inAppData.templateName]; + if (!template) { + return urls; + } + + for (CTTemplateArgument *arg in template.arguments) { + if (arg.type == CTTemplateArgumentTypeFile) { + id value = inAppData.args[arg.name]; + if (value && [value isKindOfClass:[NSString class]]) { + [urls addObject:value]; + } + } + if (arg.type == CTTemplateArgumentTypeAction) { + id value = inAppData.args[arg.name]; + if (value && [value isKindOfClass:[NSDictionary class]]) { + CTCustomTemplateInAppData *actionData = [CTCustomTemplateInAppData createWithJSON:value]; + if (actionData) { + NSSet *actionUrls = [self fileArgsURLsForInAppData:actionData]; + [urls unionSet:actionUrls]; + } + } + } + } + return urls; +} + +- (NSSet *)fileArgsURLs:(NSDictionary *)inAppJSON { + CTCustomTemplateInAppData *inAppData = [CTCustomTemplateInAppData createWithJSON:inAppJSON]; + return [self fileArgsURLsForInAppData:inAppData]; +} + +- (NSDictionary*)syncPayload { + NSMutableDictionary *payload = [NSMutableDictionary dictionary]; + payload[@"type"] = @"templatePayload"; + + NSMutableDictionary *definitions = [NSMutableDictionary dictionary]; + NSDictionary *templates = [self templates]; + [templates enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull templateKey, CTCustomTemplate * _Nonnull template, BOOL * _Nonnull stop) { + NSMutableDictionary *templateData = [NSMutableDictionary dictionary]; + templateData[@"type"] = template.templateType; + + NSMutableDictionary *groupedMap = [NSMutableDictionary dictionary]; + for (CTTemplateArgument *arg in template.arguments) { + NSArray *components = [arg.name componentsSeparatedByString:@"."]; + NSString *firstComponent = components[0]; + NSMutableArray *groupedArguments = groupedMap[firstComponent]; + if (!groupedArguments) { + groupedArguments = [NSMutableArray array]; + groupedMap[firstComponent] = groupedArguments; + } + [groupedArguments addObject:arg]; + } + + // Set the order of each argument + NSMutableSet *ordered = [NSMutableSet set]; + int order = 0; + NSMutableDictionary *arguments = [NSMutableDictionary dictionaryWithCapacity:template.arguments.count]; + for (CTTemplateArgument *arg in template.arguments) { + if (![arg.name containsString:@"."]) { + NSMutableDictionary *argument = [self argumentPayload:arg order:order]; + arguments[arg.name] = argument; + order++; + } else { + NSString *prefix = [arg.name componentsSeparatedByString:@"."][0]; + if (![ordered containsObject:prefix]) { + [ordered addObject:prefix]; + NSArray *groupedArguments = groupedMap[prefix]; + // Sort strings with dots by their first component and add them to the sorted array + NSArray *sortedArgs = [groupedArguments sortedArrayUsingComparator:^NSComparisonResult(CTTemplateArgument *arg1, CTTemplateArgument *arg2) { + return [arg1.name localizedCaseInsensitiveCompare:arg2.name]; + }]; + for (CTTemplateArgument *arg in sortedArgs) { + NSMutableDictionary *argument = [self argumentPayload:arg order:order]; + arguments[arg.name] = argument; + order++; + } + } + } + } + + templateData[@"vars"] = arguments; + definitions[template.name] = templateData; + }]; + payload[@"definitions"] = definitions; + + return payload; +} + +- (NSMutableDictionary *)argumentPayload:(CTTemplateArgument *)arg order:(int)order { + NSMutableDictionary *argument = [NSMutableDictionary new]; + id defaultValue = arg.defaultValue; + if (defaultValue) { + argument[@"defaultValue"] = defaultValue; + } + NSString *type = [CTTemplateArgument templateArgumentTypeString:arg.type]; + if (type) { + argument[@"type"] = type; + } + argument[@"order"] = @(order); + return argument; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.h b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.h new file mode 100644 index 00000000..c3e9fff5 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.h @@ -0,0 +1,22 @@ +// +// CTInAppTemplateBuilder.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTCustomTemplateBuilder.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTInAppTemplateBuilder : CTCustomTemplateBuilder + +- (instancetype)init; + +- (void)addActionArgument:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m new file mode 100644 index 00000000..fdd65c87 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m @@ -0,0 +1,24 @@ +// +// CTInAppTemplateBuilder.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTInAppTemplateBuilder.h" +#import "CTCustomTemplateBuilder-Internal.h" + +@implementation CTInAppTemplateBuilder + +- (instancetype)init { + NSSet *nullableTypes = [NSSet setWithObjects:@(CTTemplateArgumentTypeAction), nil]; + self = [super initWithType:TEMPLATE_TYPE isVisual:YES nullableArgumentTypes:nullableTypes]; + return self; +} + +- (void)addActionArgument:(nonnull NSString *)name { + [self addArgumentWithName:name type:CTTemplateArgumentTypeAction defaultValue:nil]; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.h new file mode 100644 index 00000000..39676d6a --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.h @@ -0,0 +1,32 @@ +// +// CTTemplateArgument.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, CTTemplateArgumentType) { + CTTemplateArgumentTypeString, + CTTemplateArgumentTypeNumber, + CTTemplateArgumentTypeBool, + CTTemplateArgumentTypeFile, + CTTemplateArgumentTypeAction +}; + +@interface CTTemplateArgument : NSObject + +@property (nonatomic, strong, nonnull) NSString *name; +@property (nonatomic, assign) CTTemplateArgumentType type; +@property (nonatomic, strong, nullable) id defaultValue; + +- (instancetype _Nonnull)init NS_UNAVAILABLE; +- (instancetype _Nonnull)initWithName:(NSString * _Nonnull)name + type:(CTTemplateArgumentType)type + defaultValue:(id _Nullable)defaultValue; + ++ (NSString * _Nonnull)templateArgumentTypeString:(CTTemplateArgumentType)type; + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.m b/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.m new file mode 100644 index 00000000..513318ba --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateArgument.m @@ -0,0 +1,82 @@ +// +// CTTemplateArgument.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTTemplateArgument.h" + +@implementation CTTemplateArgument + +- (instancetype)initWithName:(NSString *)name type:(CTTemplateArgumentType)type defaultValue:(id)defaultValue { + self = [super init]; + if (self) { + _name = [name copy]; + _type = type; + _defaultValue = [defaultValue copy]; + } + return self; +} + ++ (NSString *)templateArgumentTypeString:(CTTemplateArgumentType)type { + static NSDictionary *enumStringMap = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + enumStringMap = @{ + @(CTTemplateArgumentTypeString): @"string", + @(CTTemplateArgumentTypeNumber): @"number", + @(CTTemplateArgumentTypeBool): @"boolean", + @(CTTemplateArgumentTypeAction): @"action", + @(CTTemplateArgumentTypeFile): @"file", + + }; + }); + return enumStringMap[@(type)]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[CTTemplateArgument class]]) { + return NO; + } + + CTTemplateArgument *otherArgument = (CTTemplateArgument *)object; + if (![self.name isEqualToString:otherArgument.name]) { + return NO; + } + if (self.type != otherArgument.type) { + return NO; + } + if (self.defaultValue != otherArgument.defaultValue && ![self.defaultValue isEqual:otherArgument.defaultValue]) { + return NO; + } + + return YES; +} + +- (NSUInteger)hash { + NSUInteger prime = 31; + NSUInteger result = 1; + + result = prime * result + [self.name hash]; + result = prime * result + self.type; + result = prime * result + [self.defaultValue hash]; + + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> name: %@, type: %@, defaultValue: %@", + [self class], + self, + self.name, + [CTTemplateArgument templateArgumentTypeString:self.type], + self.defaultValue]; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h new file mode 100644 index 00000000..2bb21b03 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h @@ -0,0 +1,36 @@ +// +// CTTemplateContext-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTTemplateContext_Internal_h +#define CTTemplateContext_Internal_h + +#import "CTInAppNotification.h" +#import "CTCustomTemplate.h" +#import "CTTemplateContext.h" +#import "CTInAppNotificationDisplayDelegate.h" +#import "CTFileDownloader.h" + +@protocol CTTemplateContextDismissDelegate + +- (void)onDismissContext:(CTTemplateContext *)context; + +@end + +@interface CTTemplateContext (Internal) + +- (instancetype)initWithTemplate:(CTCustomTemplate *)customTemplate + notification:(CTInAppNotification *)notification + andFileDownloader:(CTFileDownloader *)fileDownloader; + +- (void)setNotificationDelegate:(id)delegate; + +- (void)setDismissDelegate:(id)delegate; + +@end + +#endif /* CTTemplateContext_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.h new file mode 100644 index 00000000..e7221240 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.h @@ -0,0 +1,72 @@ +// +// CTTemplateContext.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CTTemplateContext : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (NSString *)templateName +NS_SWIFT_NAME(name()); + +- (nullable NSString *)stringNamed:(NSString *)name +NS_SWIFT_NAME(string(name:)); + +- (nullable NSNumber *)numberNamed:(NSString *)name +NS_SWIFT_NAME(number(name:)); + +- (int)charNamed:(NSString *)name +NS_SWIFT_NAME(char(name:)); + +- (int)intNamed:(NSString *)name +NS_SWIFT_NAME(int(name:)); + +- (double)doubleNamed:(NSString *)name +NS_SWIFT_NAME(double(name:)); + +- (float)floatNamed:(NSString *)name +NS_SWIFT_NAME(float(name:)); + +- (long)longNamed:(NSString *)name +NS_SWIFT_NAME(long(name:)); + +- (long long)longLongNamed:(NSString *)name +NS_SWIFT_NAME(longLong(name:)); + +- (BOOL)boolNamed:(NSString *)name +NS_SWIFT_NAME(boolean(name:)); + +- (nullable NSDictionary *)dictionaryNamed:(NSString *)name +NS_SWIFT_NAME(dictionary(name:)); + +- (nullable NSString *)fileNamed:(NSString *)name +NS_SWIFT_NAME(file(name:)); + +/** + * Call this method to notify the SDK the template is presented. + */ +- (void)presented; + +/** + * Executes the action given by the "name" key. + * Records Notification Clicked event. + */ +- (void)triggerActionNamed:(NSString *)name +NS_SWIFT_NAME(triggerAction(name:)); + +/** + * Call this method to notify the SDK the template is dismissed. + */ +- (void)dismissed; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m new file mode 100644 index 00000000..1cf1e472 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m @@ -0,0 +1,288 @@ +// +// CTTemplateContext.m +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTTemplateContext.h" +#import "CTTemplateContext-Internal.h" +#import "CTTemplateArgument.h" +#import "CTCustomTemplate-Internal.h" +#import "CTNotificationAction.h" +#import "CTConstants.h" +#import "CTCustomTemplateBuilder.h" + +@interface CTTemplateContext () + +@property (nonatomic) CTCustomTemplate *template; +@property (nonatomic) CTInAppNotification *notification; +@property (nonatomic, strong) NSDictionary *argumentValues; +@property (nonatomic) id notificationDelegate; +@property (nonatomic) id dismissDelegate; +@property (nonatomic) BOOL isAction; +@property (nonatomic) CTFileDownloader *fileDownloader; + +@end + +@implementation CTTemplateContext + +@synthesize argumentValues = _argumentValues; + +- (instancetype)initWithTemplate:(CTCustomTemplate *)customTemplate + notification:(CTInAppNotification *)notification + andFileDownloader:(CTFileDownloader *)fileDownloader { + if (self = [super init]) { + self.notification = notification; + self.template = customTemplate; + self.isAction = notification.customTemplateInAppData.isAction; + self.fileDownloader = fileDownloader; + } + return self; +} + +- (NSString *)templateName { + return self.template.name; +} + +- (NSString *)stringNamed:(NSString *)name { + return self.argumentValues[name]; +} + +- (NSNumber *)numberNamed:(NSString *)name { + return self.argumentValues[name]; +} + +- (int)charNamed:(NSString *)name { + return [[self numberNamed:name] charValue]; +} + +- (int)intNamed:(NSString *)name { + return [[self numberNamed:name] intValue]; +} + +- (double)doubleNamed:(NSString *)name { + return [[self numberNamed:name] doubleValue]; +} + +- (float)floatNamed:(NSString *)name { + return [[self numberNamed:name] floatValue]; +} + +- (long)longNamed:(NSString *)name { + return [[self numberNamed:name] longValue]; +} + +- (long long)longLongNamed:(NSString *)name { + return [[self numberNamed:name] longLongValue]; +} + +- (BOOL)boolNamed:(NSString *)name { + return [self.argumentValues[name] boolValue]; +} + +- (NSDictionary *)dictionaryNamed:(NSString *)name { + NSString *namePrefix = [NSString stringWithFormat:@"%@.", name]; + NSArray *matchingKeys = [self.argumentValues.allKeys filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + return [evaluatedObject hasPrefix:namePrefix]; + }]]; + + if ([matchingKeys count] == 0) { + return nil; + } + + NSDictionary *matchedDictionary = [self.argumentValues dictionaryWithValuesForKeys:matchingKeys]; + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + for (NSString *key in matchedDictionary) { + NSString *subKey = [key substringWithRange:NSMakeRange(namePrefix.length, key.length - namePrefix.length)]; + NSArray *keyParts = [subKey componentsSeparatedByString:@"."]; + id value = matchedDictionary[key]; + + // If value is an action (CTNotificationAction *) return the template name or action type + id keyValue; + if ([value isKindOfClass:[CTNotificationAction class]]) { + CTNotificationAction *action = value; + keyValue = action.customTemplateInAppData.templateName ?: [CTInAppUtils inAppActionTypeString:action.type] ?: @""; + } else { + keyValue = value; + } + + /* + a.b.c = 1 + a.b.d = 2 + a.b.e.f = 3 + [dictionaryNamed:a] -> keys = [b.c, b.d, b.e.f] + result = { + b { + c: 1, + d: 2, + e: { + f: 3 + } + } + } + */ + NSMutableDictionary *currentMap = result; + for (NSUInteger i = 0; i < keyParts.count; i++) { + NSString *keyPart = keyParts[i]; + if (i == keyParts.count - 1) { + currentMap[keyPart] = keyValue; + } else { + NSMutableDictionary *innerMap = currentMap[keyPart]; + if (!innerMap) { + innerMap = [NSMutableDictionary dictionary]; + currentMap[keyPart] = innerMap; + } + + currentMap = innerMap; + } + } + } + + return [result copy]; +} + +- (NSString *)fileNamed:(NSString *)name { + return self.argumentValues[name]; +} + +- (void)presented { + if (self.isAction) { + return; + } + + if (self.notificationDelegate) { + [self.notificationDelegate notificationDidShow:self.notification]; + } else { + CleverTapLogStaticDebug(@"%@: Cannot set template as presented.", [self class]) + } +} + +- (void)triggerActionNamed:(NSString *)name { + if ([self.template.templateType isEqualToString:FUNCTION_TYPE]) { + return; + } + + id action = self.argumentValues[name]; + if (![action isKindOfClass:[CTNotificationAction class]]) { + CleverTapLogStaticDebug(@"%@: No argument of type action with name %@ for template %@.", + [self class], name, self.templateName); + return; + } + + if (self.notificationDelegate) { + CTNotificationAction *notificationAction = action; + NSString *campaignId = self.notification.campaignId ? self.notification.campaignId : @""; + NSString *cta = notificationAction.customTemplateInAppData.templateName ? notificationAction.customTemplateInAppData.templateName : name; + NSDictionary *extras = @{CLTAP_NOTIFICATION_ID_TAG:campaignId, CLTAP_PROP_WZRK_CTA: cta}; + [self.notificationDelegate handleNotificationAction:notificationAction forNotification:self.notification withExtras:extras]; + } +} + +- (void)dismissed { + if (self.dismissDelegate) { + [self.dismissDelegate onDismissContext:self]; + self.dismissDelegate = nil; + } + + // If the context is an action and visual:false, + // it does not go through the in-app queue, so the dismiss is NOOP. + // If the context is not an action, then it goes through the in-app queue no matter + // the visual property i.e standalone function + if (self.isAction && !self.template.isVisual) { + return; + } + + if (self.notificationDelegate) { + [self.notificationDelegate notificationDidDismiss:self.notification fromViewController:nil]; + self.notificationDelegate = nil; + } else { + CleverTapLogStaticDebug(@"%@: Cannot set template as dismissed.", [self class]) + } +} + +- (NSDictionary *)argumentValues { + if (_argumentValues) { + return _argumentValues; + } + _argumentValues = [self mergeArguments]; + return _argumentValues; +} + +- (NSDictionary *)mergeArguments { + NSMutableDictionary *merged = [NSMutableDictionary new]; + for (CTTemplateArgument *arg in self.template.arguments) { + merged[arg.name] = arg.defaultValue; + id override = [self valueForArgument:arg]; + if (override) { + merged[arg.name] = override; + } + } + + return [merged copy]; +} + +- (id)valueForArgument:(CTTemplateArgument *)arg { + NSDictionary *overrides = self.notification.customTemplateInAppData.args; + id override = overrides[arg.name]; + if (override) { + switch (arg.type) { + case CTTemplateArgumentTypeString: + if ([override isKindOfClass:[NSString class]]) { + return override; + } + break; + case CTTemplateArgumentTypeNumber: + case CTTemplateArgumentTypeBool: + if ([override isKindOfClass:[NSNumber class]]) { + return override; + } + break; + case CTTemplateArgumentTypeFile: + if ([override isKindOfClass:[NSString class]]) { + return [self.fileDownloader fileDownloadPath:override]; + } + break; + case CTTemplateArgumentTypeAction: { + CTNotificationAction *action = [[CTNotificationAction alloc] initWithJSON:override[CLTAP_INAPP_ACTIONS]]; + if (action && !action.error) { + return action; + } else if (action.error) { + CleverTapLogStaticDebug(@"%@: Error creating action for argument: %@. Error: %@", [self class], arg.name, action.error); + } + break; + } + default: + break; + } + } + return nil; +} + +- (NSString *)debugDescription { + NSMutableArray *argsDescription = [NSMutableArray array]; + for (NSString *key in self.argumentValues) { + NSString *value; + if ([self.argumentValues[key] isKindOfClass:[CTNotificationAction class]]) { + CTNotificationAction *action = self.argumentValues[key]; + NSString *name = action.customTemplateInAppData.templateName ? action.customTemplateInAppData.templateName : @""; + value = [NSString stringWithFormat:@"Action: %@", name]; + } else { + value = [self.argumentValues[key] debugDescription]; + } + [argsDescription addObject:[NSString stringWithFormat:@"%@: %@", key, value]]; + } + NSString *argsString = @"{\n}"; + if (argsDescription.count > 0) { + argsString = [NSString stringWithFormat:@"{\n%@\n}", [argsDescription componentsJoinedByString:@",\n"]]; + } + return [NSString stringWithFormat:@"<%@: %p> templateName: %@, args: %@", + [self class], + self, + self.templateName, + argsString]; +} + +@end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplatePresenter.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplatePresenter.h new file mode 100644 index 00000000..c0bd66c1 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplatePresenter.h @@ -0,0 +1,28 @@ +// +// TemplatePresenter.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTTemplatePresenter_h +#define CTTemplatePresenter_h + +#import "CTTemplateContext.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol CTTemplatePresenter + +- (void)onPresent:(CTTemplateContext *)context +NS_SWIFT_NAME(onPresent(context:)); + +- (void)onCloseClicked:(CTTemplateContext *)context +NS_SWIFT_NAME(onCloseClicked(context:)); + +@end + +NS_ASSUME_NONNULL_END + +#endif /* TemplatePresenter_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateProducer.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplateProducer.h new file mode 100644 index 00000000..64d679a6 --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateProducer.h @@ -0,0 +1,20 @@ +// +// CTTemplateProducer.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 27.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTTemplateProducer_h +#define CTTemplateProducer_h + +#import "CTCustomTemplate.h" +#import "CleverTapInstanceConfig.h" + +@protocol CTTemplateProducer + +- (NSSet * _Nonnull)defineTemplates:(CleverTapInstanceConfig * _Nonnull)instanceConfig; + +@end +#endif /* TemplateProducer_h */ diff --git a/CleverTapSDK/InApps/Matchers/CTEventAdapter.h b/CleverTapSDK/InApps/Matchers/CTEventAdapter.h index dca2635c..7bc79c75 100644 --- a/CleverTapSDK/InApps/Matchers/CTEventAdapter.h +++ b/CleverTapSDK/InApps/Matchers/CTEventAdapter.h @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) NSString *eventName; @property (nonatomic, assign, readonly) CLLocationCoordinate2D location; +@property (nonatomic, strong, readonly) NSString *profileAttrName; +@property (nonatomic, strong, nonnull) NSDictionary *eventProperties; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithEventName:(NSString *)eventName @@ -27,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN location:(CLLocationCoordinate2D)location andItems:(NSArray *)items; +- (instancetype)initWithEventName:(NSString *)eventName + profileAttrName:(NSString *)profileAttrName + eventProperties:(NSDictionary *)eventProperties + andLocation:(CLLocationCoordinate2D)location; + - (CTTriggerValue * _Nullable)propertyValueNamed:(NSString *)name; - (CTTriggerValue * _Nullable)itemValueNamed:(NSString *)name; diff --git a/CleverTapSDK/InApps/Matchers/CTEventAdapter.m b/CleverTapSDK/InApps/Matchers/CTEventAdapter.m index 35df268e..301f551f 100644 --- a/CleverTapSDK/InApps/Matchers/CTEventAdapter.m +++ b/CleverTapSDK/InApps/Matchers/CTEventAdapter.m @@ -14,9 +14,9 @@ @interface CTEventAdapter() @property (nonatomic, strong) NSString *eventName; -@property (nonatomic, strong) NSDictionary *eventProperties; @property (nonatomic, strong) NSArray *items; @property (nonatomic, assign) CLLocationCoordinate2D location; +@property (nonatomic, strong, nullable) NSString *profileAttrName; @end @@ -74,6 +74,18 @@ - (instancetype)initWithEventName:(NSString *)eventName return self; } +- (instancetype)initWithEventName:(NSString *)eventName + profileAttrName:(NSString *)profileAttrName + eventProperties:(NSDictionary *)eventProperties + andLocation:(CLLocationCoordinate2D)location{ + + if (self = [super init]) { + self = [self initWithEventName:eventName eventProperties:eventProperties location:location andItems:@[]]; + self.profileAttrName = profileAttrName; + } + return self; +} + - (CTTriggerValue *)propertyValueNamed:(NSString *)name { id propertyValue = [self getActualPropertyValue:name]; if (propertyValue) { diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h index a500a4a8..6f326107 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSInteger propertyCount; @property (nonatomic, readonly) NSInteger itemsCount; @property (nonatomic, readonly) NSInteger geoRadiusCount; +@property (nonatomic, strong, readonly) NSString *profileAttrName; - (CTTriggerCondition * _Nullable)propertyAtIndex:(NSInteger)index; - (CTTriggerCondition * _Nullable)itemAtIndex:(NSInteger)index; diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m index 83bb7f0f..c6e2eb0a 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m @@ -17,6 +17,7 @@ @interface CTTriggerAdapter() @property (nonatomic, strong) NSArray *items; @property (nonatomic, strong) NSArray *geoRadius; +@property (nonatomic, strong) NSString *profileAttrName; @end @@ -28,6 +29,7 @@ - (instancetype)initWithJSON:(NSDictionary *)triggerJSON { self.properties = triggerJSON[@"eventProperties"]; self.items = triggerJSON[@"itemProperties"]; self.geoRadius = triggerJSON[@"geoRadius"]; + self.profileAttrName = triggerJSON[@"profileAttrName"]; } return self; } diff --git a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m index 1766d49c..b6366b13 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m @@ -30,7 +30,10 @@ - (BOOL)matchEventWhenTriggers:(NSArray *)whenTriggers event:(CTEventAdapter *)e } - (BOOL)match:(CTTriggerAdapter *)trigger event:(CTEventAdapter *)event { - if (![[event eventName] isEqualToString:[trigger eventName]]) { + + BOOL eventNameMatch = [[event eventName] isEqualToString:[trigger eventName]]; + BOOL profileAttrNameMatch = [event profileAttrName] != nil && [[event profileAttrName] isEqualToString:[trigger profileAttrName]]; + if (!eventNameMatch && !profileAttrNameMatch) { return NO; } diff --git a/CleverTapSDK/ProductExperiences/CTVar-Internal.h b/CleverTapSDK/ProductExperiences/CTVar-Internal.h index 10954bc5..b061e66e 100644 --- a/CleverTapSDK/ProductExperiences/CTVar-Internal.h +++ b/CleverTapSDK/ProductExperiences/CTVar-Internal.h @@ -16,12 +16,16 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly) BOOL hadStarted; @property (readonly, strong) NSString *kind; @property (readonly, strong) NSMutableArray *valueChangedBlocks; +@property (readonly, strong) NSMutableArray *fileReadyBlocks; @property (nonatomic, unsafe_unretained, nullable) id delegate; @property (readonly) BOOL hasChanged; +@property (readonly) BOOL shouldDownloadFile; +@property (readonly, strong, nullable) NSString *fileURL; -- (void)update; +- (BOOL)update; - (void)cacheComputedValues; - (void)triggerValueChanged; +- (void)triggerFileIsReady; + (BOOL)printedCallbackWarning; + (void)setPrintedCallbackWarning:(BOOL)newPrintedCallbackWarning; diff --git a/CleverTapSDK/ProductExperiences/CTVar.h b/CleverTapSDK/ProductExperiences/CTVar.h index 8c347875..368ea546 100644 --- a/CleverTapSDK/ProductExperiences/CTVar.h +++ b/CleverTapSDK/ProductExperiences/CTVar.h @@ -17,6 +17,10 @@ NS_SWIFT_NAME(VarDelegate) * Called when the value of the variable changes. */ - (void)valueDidChange:(CTVar *)variable; +/** + * Called when the file is downloaded and ready. + */ +- (void)fileIsReady:(CTVar *)var; @end /** @@ -30,6 +34,7 @@ NS_SWIFT_NAME(Var) @property (readonly, strong, nullable) NSNumber *numberValue; @property (readonly, strong, nullable) id value; @property (readonly, strong, nullable) id defaultValue; +@property (readonly, strong, nullable) NSString *fileValue; /** * @{ @@ -67,6 +72,11 @@ NS_SWIFT_NAME(Var) */ - (void)onValueChanged:(CleverTapVariablesChangedBlock)block; +/** + * Called when the value of the file variable is downloaded and ready. + */ +- (void)onFileIsReady:(CleverTapVariablesChangedBlock)block; + /** * Sets the delegate of the variable in order to use * {@link CTVarDelegate::valueDidChange:} @@ -102,6 +112,7 @@ NS_SWIFT_NAME(Var) - (NSUInteger)unsignedIntegerValue; - (unsigned long)unsignedLongValue; - (unsigned long long)unsignedLongLongValue; +- (nullable NSString *)fileValue; /**@}*/ @end diff --git a/CleverTapSDK/ProductExperiences/CTVar.m b/CleverTapSDK/ProductExperiences/CTVar.m index 53499d53..a7e7f389 100644 --- a/CleverTapSDK/ProductExperiences/CTVar.m +++ b/CleverTapSDK/ProductExperiences/CTVar.m @@ -10,19 +10,22 @@ @interface CTVar (PrivateProperties) @property (nonatomic, strong) NSArray *nameComponents; @property (nonatomic, strong) NSString *stringValue; @property (nonatomic, strong) NSNumber *numberValue; +@property (nonatomic, strong) NSString *fileValue; +@property (nonatomic, strong) NSString *fileURL; @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, strong) NSMutableArray *fileReadyBlocks; @property (nonatomic) BOOL hasChanged; +@property (nonatomic) BOOL shouldDownloadFile; @end @implementation CTVar - (instancetype)initWithName:(NSString *)name withDefaultValue:(NSNumber *)defaultValue - withKind:(NSString *)kind varCache:(CTVarCache *)cache -{ + withKind:(NSString *)kind varCache:(CTVarCache *)cache { self = [super init]; if (self) { CT_TRY @@ -34,9 +37,21 @@ - (instancetype)initWithName:(NSString *)name withDefaultValue:(NSNumber *)defau _kind = kind; [self cacheComputedValues]; + // If the file is defined after the Vars request has completed, + // file needs to be downloaded individually + if ([_kind isEqualToString:CT_KIND_FILE] && [[self varCache] hasVarsRequestCompleted]) { + _shouldDownloadFile = YES; + } + [self.varCache registerVariable:self]; [self update]; + + // Store the actual file URL as _value returns path of file downloaded + // after the var update + if ([_kind isEqualToString:CT_KIND_FILE]) { + _fileURL = _value; + } CT_END_TRY } return self; @@ -45,9 +60,11 @@ - (instancetype)initWithName:(NSString *)name withDefaultValue:(NSNumber *)defau // Manually @synthesize since CTVar provides custom getters/setters // Properties are defined as readonly in CTVar-Internal // and readwrite in PrivateProperties category +@synthesize value = _value; @synthesize stringValue = _stringValue; @synthesize numberValue = _numberValue; @synthesize varCache = _varCache; +@synthesize fileValue = _fileValue; - (CTVarCache *)varCache { return _varCache; @@ -59,7 +76,7 @@ - (void)setVarCache:(CTVarCache *)varCache { #pragma mark Updates -- (void) cacheComputedValues { +- (void)cacheComputedValues { // Cache computed values. if ([_value isKindOfClass:NSString.class]) { _stringValue = (NSString *) _value; @@ -73,23 +90,35 @@ - (void) cacheComputedValues { } } -- (void)update { +- (BOOL)update { NSObject *oldValue = _value; _value = [self.varCache getMergedValueFromComponentArray:_nameComponents]; if ([_value isEqual:oldValue] && _hadStarted) { - return; + return NO; } [self cacheComputedValues]; + BOOL changed = NO; if (![_value isEqual:oldValue]) { _hasChanged = YES; + changed = YES; + // Update _fileURL with new value if it has changed. + if ([_kind isEqualToString:CT_KIND_FILE]) { + _fileURL = _value; + } + } + + if (_shouldDownloadFile) { + [self.varCache fileVarUpdated:self]; } if ([[self varCache] hasVarsRequestCompleted]) { [self triggerValueChanged]; _hadStarted = YES; + _shouldDownloadFile = NO; } + return changed; } #pragma mark Callbacks @@ -100,8 +129,10 @@ - (void)triggerValueChanged { [self.delegate valueDidChange:self]; } - for (CleverTapVariablesChangedBlock block in _valueChangedBlocks.copy) { - block(); + if (_valueChangedBlocks && _valueChangedBlocks.count > 0) { + for (CleverTapVariablesChangedBlock block in _valueChangedBlocks.copy) { + block(); + } } } @@ -128,20 +159,72 @@ - (void)setDelegate:(id)delegate { if ([[self varCache] hasVarsRequestCompleted]) { [self triggerValueChanged]; } + + // Call fileIsReady if value is already fetched and file is already present. + if ([_kind isEqualToString:CT_KIND_FILE] && _fileURL) { + if ([self.varCache isFileAlreadyPresent:_fileURL]) { + [self triggerFileIsReady]; + } + } CT_END_TRY } +#pragma mark File Handling + +- (void)onFileIsReady:(CleverTapVariablesChangedBlock)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CTVar onFileIsReady]."); + return; + } + + if (![_kind isEqualToString:CT_KIND_FILE]) { + CleverTapLogStaticDebug(@"[CTVar onFileIsReady] is only available for File Variables."); + return; + } + + CT_TRY + if (!_fileReadyBlocks) { + _fileReadyBlocks = [NSMutableArray array]; + } + [_fileReadyBlocks addObject:[block copy]]; + if ([self.varCache isFileAlreadyPresent:_fileURL]) { + [self triggerFileIsReady]; + } + CT_END_TRY +} + +- (void)triggerFileIsReady { + if (self.delegate && + [self.delegate respondsToSelector:@selector(fileIsReady:)]) { + [self.delegate fileIsReady:self]; + } + + if (_fileReadyBlocks && _fileReadyBlocks.count > 0) { + for (CleverTapVariablesChangedBlock block in _fileReadyBlocks.copy) { + block(); + } + } +} + +- (nullable NSString *)fileValue { + [self warnIfNotStarted]; + if ([_kind isEqualToString:CT_KIND_FILE]) { + return [self.varCache fileDownloadPath:_fileURL]; + } + return nil; +} + #pragma mark Dictionary handling -- (id) objectForKey:(NSString *)key { +- (id)objectForKey:(NSString *)key { return [self objectForKeyPath:key, nil]; } -- (id) objectAtIndex:(NSUInteger)index { +- (id)objectAtIndex:(NSUInteger)index { return [self objectForKeyPath:@(index), nil]; } -- (id) objectForKeyPath:(id)firstComponent, ... { +- (id)objectForKeyPath:(id)firstComponent, ... { CT_TRY [self warnIfNotStarted]; NSMutableArray *components = [_nameComponents mutableCopy]; @@ -169,6 +252,14 @@ - (id)objectForKeyPathComponents:(NSArray *)pathComponents { #pragma mark Value accessors +- (id)value { + [self warnIfNotStarted]; + if ([_kind isEqualToString:CT_KIND_FILE]) { + return [self.varCache fileDownloadPath:_fileURL]; + } + return _value; +} + - (NSNumber *)numberValue { [self warnIfNotStarted]; return _numberValue; @@ -176,6 +267,9 @@ - (NSNumber *)numberValue { - (NSString *)stringValue { [self warnIfNotStarted]; + if ([_kind isEqualToString:CT_KIND_FILE]) { + return [self.varCache fileDownloadPath:_fileURL]; + } return _stringValue; } diff --git a/CleverTapSDK/ProductExperiences/CTVarCache.h b/CleverTapSDK/ProductExperiences/CTVarCache.h index 9a8a7996..81ca14ab 100644 --- a/CleverTapSDK/ProductExperiences/CTVarCache.h +++ b/CleverTapSDK/ProductExperiences/CTVarCache.h @@ -2,6 +2,12 @@ #import "CTVar-Internal.h" #import "CleverTapInstanceConfig.h" #import "CTDeviceInfo.h" +#import "CTFileDownloader.h" + +@protocol CTFileVarDelegate +@required +- (void)triggerNoDownloadsPending; +@end NS_ASSUME_NONNULL_BEGIN @@ -12,11 +18,15 @@ NS_SWIFT_NAME(VarCache) - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo; +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config + deviceInfo:(CTDeviceInfo*)deviceInfo + fileDownloader:(CTFileDownloader *)fileDownloader; @property (nonatomic, strong, readonly) CleverTapInstanceConfig *config; -@property (strong, nonatomic) NSMutableDictionary *vars; +@property (strong, nonatomic) NSMutableDictionary *vars; @property (assign, nonatomic) BOOL hasVarsRequestCompleted; +@property (assign, nonatomic) BOOL hasPendingDownloads; +@property (nonatomic, weak) id delegate; - (nullable NSDictionary *)diffs; - (void)loadDiffs; @@ -30,6 +40,10 @@ NS_SWIFT_NAME(VarCache) - (nullable id)getMergedValueFromComponentArray:(NSArray *) components; - (void)clearUserContent; +- (nullable NSString *)fileDownloadPath:(NSString *)fileURL; +- (BOOL)isFileAlreadyPresent:(NSString *)fileURL; +- (void)fileVarUpdated:(CTVar *)fileVar; + @end NS_ASSUME_NONNULL_END diff --git a/CleverTapSDK/ProductExperiences/CTVarCache.m b/CleverTapSDK/ProductExperiences/CTVarCache.m index ba4bf759..d0f96d2d 100644 --- a/CleverTapSDK/ProductExperiences/CTVarCache.m +++ b/CleverTapSDK/ProductExperiences/CTVarCache.m @@ -12,14 +12,19 @@ @interface CTVarCache() @property (strong, nonatomic) CacheUpdateBlock updateBlock; @property (nonatomic, strong) CleverTapInstanceConfig *config; @property (nonatomic, strong) CTDeviceInfo *deviceInfo; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; +@property (strong, nonatomic) NSMutableDictionary *fileVarsInDownload; @end @implementation CTVarCache -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo { +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config + deviceInfo:(CTDeviceInfo*)deviceInfo + fileDownloader:(CTFileDownloader *)fileDownloader { if ((self = [super init])) { self.config = config; self.deviceInfo = deviceInfo; + self.fileDownloader = fileDownloader; [self initialize]; } return self; @@ -30,6 +35,8 @@ - (void)initialize { self.diffs = [NSMutableDictionary dictionary]; self.valuesFromClient = [NSMutableDictionary dictionary]; self.hasVarsRequestCompleted = NO; + self.hasPendingDownloads = NO; + self.fileVarsInDownload = [NSMutableDictionary dictionary]; } - (NSArray *)getNameComponents:(NSString *)name { @@ -171,7 +178,7 @@ - (void)loadDiffs { return; } NSKeyedUnarchiver *unarchiver; - if (@available(iOS 12.0, *)) { + if (@available(iOS 12.0, tvOS 11.0, *)) { NSError *error = nil; unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:diffsData error:&error]; if (error != nil) { @@ -237,8 +244,22 @@ - (void)applyVariableDiffs:(NSDictionary *)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. + NSMutableArray *updatedFileVariables = [NSMutableArray array]; for (NSString *name in [self.vars allKeys]) { - [self.vars[name] update]; + CTVar *var = self.vars[name]; + // Always update the variable + BOOL hasChanged = [var update]; + if (hasChanged && [var.kind isEqualToString:CT_KIND_FILE]) { + [updatedFileVariables addObject:var]; + } + } + + NSMutableArray *fileURLs = [self fileURLs:updatedFileVariables]; + if ([fileURLs count] > 0) { + [self setHasPendingDownloads:YES]; + [self startFileDownload:fileURLs]; + } else { + [self.delegate triggerNoDownloadsPending]; } } else { CleverTapLogDebug(self.config.logLevel, @"%@: No variables received from the server", self); @@ -262,4 +283,94 @@ - (void)clearUserContent { }]; } +#pragma mark - File Handling + +- (void)startFileDownload:(NSMutableArray *)fileURLs { + [self.fileDownloader downloadFiles:fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + NSMutableArray *retryURLs = [NSMutableArray new]; + // Call fileIsReady for variables whose files are downloaded. + for (NSString *key in status.allKeys) { + if ([status[key] boolValue]) { + @synchronized (self) { + [self.fileVarsInDownload[key] triggerFileIsReady]; + [self.fileVarsInDownload removeObjectForKey:key]; + } + } else { + [retryURLs addObject:key]; + } + } + // Retry once if a URL failed to download + if (retryURLs.count == 0) { + [self setHasPendingDownloads:NO]; + [self.delegate triggerNoDownloadsPending]; + } else { + [self retryFileDownload:retryURLs]; + } + }]; +} + +- (nullable NSString *)fileDownloadPath:(NSString *)fileURL { + return [self.fileDownloader fileDownloadPath:fileURL]; +} + +- (BOOL)isFileAlreadyPresent:(NSString *)fileURL { + return [self.fileDownloader isFileAlreadyPresent:fileURL andUpdateExpiryTime:YES]; +} + +- (NSMutableArray *)fileURLs:(NSArray *)fileVars { + NSMutableArray *downloadURLs = [NSMutableArray new]; + for (CTVar *var in fileVars) { + if (var.fileURL) { + // If file is already present, call fileIsReady + // else download the file and call fileIsReady when downloaded + if ([self isFileAlreadyPresent:var.fileURL]) { + [var triggerFileIsReady]; + } else { + [downloadURLs addObject:var.fileURL]; + @synchronized (self) { + [self.fileVarsInDownload setObject:var forKey:var.fileURL]; + } + } + } else { + // Trigger FileIsReady since the value changed to null (no override) + [var triggerFileIsReady]; + } + } + + return downloadURLs; +} + +- (void)retryFileDownload:(NSMutableArray *)urls { + [self.fileDownloader downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [self setHasPendingDownloads:NO]; + for (NSString *key in status.allKeys) { + @synchronized (self) { + if ([status[key] boolValue]) { + [self.fileVarsInDownload[key] triggerFileIsReady]; + } + [self.fileVarsInDownload removeObjectForKey:key]; + } + } + [self.delegate triggerNoDownloadsPending]; + }]; +} + +- (void)fileVarUpdated:(CTVar *)fileVar { + NSString *url = fileVar.fileURL; + if (!url) { + // FileIsReady is not triggered if there is no override, fileURL is nil + return; + } + + if ([self isFileAlreadyPresent:url]) { + [fileVar triggerFileIsReady]; + } else { + [self.fileDownloader downloadFiles:@[url] withCompletionBlock:^(NSDictionary * _Nonnull status) { + if ([status[url] boolValue]) { + [fileVar triggerFileIsReady]; + } + }]; + } +} + @end diff --git a/CleverTapSDK/ProductExperiences/CTVariables.h b/CleverTapSDK/ProductExperiences/CTVariables.h index 8a15b2b2..8ea4cb64 100644 --- a/CleverTapSDK/ProductExperiences/CTVariables.h +++ b/CleverTapSDK/ProductExperiences/CTVariables.h @@ -10,15 +10,18 @@ #import "CTVarCache.h" #import "CleverTapInstanceConfig.h" #import "CTDeviceInfo.h" +#import "CTFileDownloader.h" NS_ASSUME_NONNULL_BEGIN -@interface CTVariables : NSObject +@interface CTVariables : NSObject @property(strong, nonatomic) CTVarCache *varCache; @property(strong, nonatomic, nullable) CleverTapFetchVariablesBlock fetchVariablesBlock; -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo *)deviceInfo; +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config + deviceInfo:(CTDeviceInfo *)deviceInfo + fileDownloader:(CTFileDownloader *)fileDownloader; - (CTVar * _Nullable)define:(NSString *)name with:(nullable NSObject *)defaultValue @@ -30,6 +33,8 @@ NS_SWIFT_NAME(define(name:value:kind:)); - (void)triggerFetchVariables:(BOOL)success; - (void)onVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block; - (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block; +- (void)onVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull)block; +- (void)onceVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull)block; - (NSDictionary*)flatten:(NSDictionary*)map varName:(NSString*)varName; - (NSDictionary*)varsPayload; - (NSDictionary*)unflatten:(NSDictionary*)result; diff --git a/CleverTapSDK/ProductExperiences/CTVariables.m b/CleverTapSDK/ProductExperiences/CTVariables.m index fc607283..ac3e7235 100644 --- a/CleverTapSDK/ProductExperiences/CTVariables.m +++ b/CleverTapSDK/ProductExperiences/CTVariables.m @@ -17,13 +17,19 @@ @interface CTVariables() @property(strong, nonatomic) NSMutableArray *variablesChangedBlocks; @property(strong, nonatomic) NSMutableArray *onceVariablesChangedBlocks; +@property(strong, nonatomic) NSMutableArray *noFileDownloadsBlocks; +@property(strong, nonatomic) NSMutableArray *onceNoFileDownloadsBlocks; + @end @implementation CTVariables -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CTDeviceInfo*)deviceInfo { +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config + deviceInfo:(CTDeviceInfo*)deviceInfo + fileDownloader:(CTFileDownloader *)fileDownloader { if ((self = [super init])) { - self.varCache = [[CTVarCache alloc] initWithConfig:config deviceInfo:deviceInfo]; + self.varCache = [[CTVarCache alloc] initWithConfig:config deviceInfo:deviceInfo fileDownloader:fileDownloader]; + [self.varCache setDelegate:self]; } return self; } @@ -164,6 +170,67 @@ - (void)onceVariablesChanged:(CleverTapVariablesChangedBlock _Nonnull)block { } } +- (void)triggerVariablesChangedAndNoDownloadsPending { + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self triggerVariablesChangedAndNoDownloadsPending]; + }); + return; + } + + for (CleverTapVariablesChangedBlock block in self.noFileDownloadsBlocks.copy) { + block(); + } + + NSArray *onceBlocksCopy; + @synchronized (self.onceNoFileDownloadsBlocks) { + onceBlocksCopy = self.onceNoFileDownloadsBlocks.copy; + [self.onceNoFileDownloadsBlocks removeAllObjects]; + } + for (CleverTapVariablesChangedBlock block in onceBlocksCopy) { + block(); + } +} + +- (void)onVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CleverTap onVariablesChangedAndNoDownloadsPending]."); + return; + } + + CT_TRY + if (!self.noFileDownloadsBlocks) { + self.noFileDownloadsBlocks = [NSMutableArray array]; + } + [self.noFileDownloadsBlocks addObject:[block copy]]; + CT_END_TRY + + if ([self.varCache hasVarsRequestCompleted] && ![self.varCache hasPendingDownloads]) { + block(); + } +} + +- (void)onceVariablesChangedAndNoDownloadsPending:(CleverTapVariablesChangedBlock _Nonnull)block { + if (!block) { + CleverTapLogStaticDebug(@"Nil block parameter provided while calling [CleverTap onceVariablesChangedAndNoDownloadsPending]."); + return; + } + + if ([self.varCache hasVarsRequestCompleted] && ![self.varCache hasPendingDownloads]) { + block(); + } else { + CT_TRY + static dispatch_once_t onceBlocksToken; + dispatch_once(&onceBlocksToken, ^{ + self.onceNoFileDownloadsBlocks = [NSMutableArray array]; + }); + @synchronized (self.onceNoFileDownloadsBlocks) { + [self.onceNoFileDownloadsBlocks addObject:[block copy]]; + } + CT_END_TRY + } +} + #pragma mark Vars Payload - (NSDictionary*)varsPayload { NSMutableDictionary *payload = [NSMutableDictionary dictionary]; @@ -237,7 +304,10 @@ - (NSDictionary*)unflatten:(NSDictionary*)flatDictionary { currentMap = nestedMap; } else { - currentMap = ((NSMutableDictionary*)currentMap[component]); + if (![currentMap[component] isKindOfClass:[NSMutableDictionary class]] && [currentMap[component] isKindOfClass:[NSDictionary class]]) { + currentMap[component] = [[NSMutableDictionary alloc] initWithDictionary:currentMap[component]]; + } + currentMap = currentMap[component]; } } if ([currentMap isKindOfClass:[NSMutableDictionary class]]) { @@ -252,4 +322,10 @@ - (NSDictionary*)unflatten:(NSDictionary*)flatDictionary { return unflattenVars; } +#pragma mark CTFileVarDelegate + +- (void)triggerNoDownloadsPending { + [self triggerVariablesChangedAndNoDownloadsPending]; +} + @end diff --git a/CleverTapSDK/include/CTAppFunctionBuilder.h b/CleverTapSDK/include/CTAppFunctionBuilder.h new file mode 120000 index 00000000..fb862041 --- /dev/null +++ b/CleverTapSDK/include/CTAppFunctionBuilder.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTAppFunctionBuilder.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTCustomTemplate.h b/CleverTapSDK/include/CTCustomTemplate.h new file mode 120000 index 00000000..202d3154 --- /dev/null +++ b/CleverTapSDK/include/CTCustomTemplate.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTCustomTemplate.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTCustomTemplateBuilder.h b/CleverTapSDK/include/CTCustomTemplateBuilder.h new file mode 120000 index 00000000..052ccd27 --- /dev/null +++ b/CleverTapSDK/include/CTCustomTemplateBuilder.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTCustomTemplateBuilder.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTCustomTemplatesManager.h b/CleverTapSDK/include/CTCustomTemplatesManager.h new file mode 120000 index 00000000..4bb168cd --- /dev/null +++ b/CleverTapSDK/include/CTCustomTemplatesManager.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTCustomTemplatesManager.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTInAppTemplateBuilder.h b/CleverTapSDK/include/CTInAppTemplateBuilder.h new file mode 120000 index 00000000..ee7fa924 --- /dev/null +++ b/CleverTapSDK/include/CTInAppTemplateBuilder.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTInAppTemplateBuilder.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTTemplateContext.h b/CleverTapSDK/include/CTTemplateContext.h new file mode 120000 index 00000000..96f627ce --- /dev/null +++ b/CleverTapSDK/include/CTTemplateContext.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTTemplateContext.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTTemplatePresenter.h b/CleverTapSDK/include/CTTemplatePresenter.h new file mode 120000 index 00000000..85cec938 --- /dev/null +++ b/CleverTapSDK/include/CTTemplatePresenter.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTTemplatePresenter.h \ No newline at end of file diff --git a/CleverTapSDK/include/CTTemplateProducer.h b/CleverTapSDK/include/CTTemplateProducer.h new file mode 120000 index 00000000..5d45890b --- /dev/null +++ b/CleverTapSDK/include/CTTemplateProducer.h @@ -0,0 +1 @@ +../InApps/CustomTemplates/CTTemplateProducer.h \ No newline at end of file diff --git a/CleverTapSDK/ios.modulemap b/CleverTapSDK/ios.modulemap index 4ea67b6e..9d8d1e31 100644 --- a/CleverTapSDK/ios.modulemap +++ b/CleverTapSDK/ios.modulemap @@ -21,5 +21,13 @@ framework module CleverTapSDK { header "CleverTap+CTVar.h" header "CTVar.h" header "LeanplumCT.h" + header "CTInAppTemplateBuilder.h" + header "CTAppFunctionBuilder.h" + header "CTTemplatePresenter.h" + header "CTTemplateProducer.h" + header "CTCustomTemplateBuilder.h" + header "CTCustomTemplate.h" + header "CTTemplateContext.h" + header "CTCustomTemplatesManager.h" export * } diff --git a/CleverTapSDKTests/CTEventBuilderTest.m b/CleverTapSDKTests/CTEventBuilderTest.m index c043eb81..7a172509 100644 --- a/CleverTapSDKTests/CTEventBuilderTest.m +++ b/CleverTapSDKTests/CTEventBuilderTest.m @@ -10,17 +10,13 @@ #import "CTEventBuilder.h" #import "CTValidator.h" #import "CTInAppNotification.h" -#import "InAppHelper.h" @interface CTEventBuilderTest : XCTestCase -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; @end @implementation CTEventBuilderTest - (void)setUp { - InAppHelper *helper = [InAppHelper new]; - self.prefetchManager = helper.imagePrefetchManager; } - (void)tearDown { @@ -283,7 +279,7 @@ - (void)test_buildPushNotificationEvent_withClickedFalse { - (void)test_buildInAppNotificationStateEvent_withClickedTrueAndInvalidKey { NSDictionary *notification = @{@"notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:true forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { @@ -296,7 +292,7 @@ - (void)test_buildInAppNotificationStateEvent_withClickedTrueAndInvalidKey { - (void)test_buildInAppNotificationStateEvent_withClickedFalseAndInvalidKey { NSDictionary *notification = @{@"notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:false forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { @@ -309,7 +305,7 @@ - (void)test_buildInAppNotificationStateEvent_withClickedFalseAndInvalidKey { - (void)test_buildInAppNotificationStateEvent_withValidKey { NSDictionary *notification = @{@"wzrk_notiKey": @"notiValue"}; - CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *inAppNotification = [[CTInAppNotification alloc] initWithJSON:notification]; NSDictionary *queryParam = @{@"key1": @"value1"}; [CTEventBuilder buildInAppNotificationStateEvent:false forNotification:inAppNotification andQueryParameters:queryParam completionHandler:^(NSDictionary * _Nullable event, NSArray * _Nullable errors) { diff --git a/CleverTapSDKTests/CTLocalDataStoreTests.m b/CleverTapSDKTests/CTLocalDataStoreTests.m new file mode 100644 index 00000000..270d2804 --- /dev/null +++ b/CleverTapSDKTests/CTLocalDataStoreTests.m @@ -0,0 +1,112 @@ +// +// CTLocalDataStoreTests.m +// CleverTapSDKTests +// +// Created by Kushagra Mishra on 04/07/24. +// Copyright © 2024 CleverTap. All rights reserved. +// +#import +#import +#import "CTLocalDataStore.h" +#import "CTProfileBuilder.h" +#import "CTConstants.h" + +@interface CTLocalDataStoreTests : XCTestCase +@property (nonatomic, strong) CTLocalDataStore *dataStore; +@property (nonatomic, strong) id dataStoreMock; +@property (nonatomic, strong) id profileBuilderMock; +@end + +@implementation CTLocalDataStoreTests + +- (void)setUp { + [super setUp]; + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccount" accountToken:@"testToken" accountRegion:@"testRegion"]; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"testDeviceInfo"]; + CTDispatchQueueManager *queueManager = [[CTDispatchQueueManager alloc] initWithConfig:config]; + self.dataStore = [[CTLocalDataStore alloc] initWithConfig:config profileValues:[NSMutableDictionary new] andDeviceInfo:deviceInfo dispatchQueueManager:queueManager]; +} + +- (void)tearDown { + self.dataStore = nil; + [super tearDown]; +} + +- (void)testGetUserAttributeChangePropertiesWithEmptyEvent { + NSDictionary *event = @{}; + NSDictionary *result = [self.dataStore getUserAttributeChangeProperties:event]; + XCTAssertEqual(result.count, 0); +} + +- (void)testGetUserAttributeChangePropertiesWithNoProfile { + NSDictionary *event = @{@"someKey": @"someValue"}; + NSDictionary *result = [self.dataStore getUserAttributeChangeProperties:event]; + XCTAssertEqual(result.count, 0); +} + +- (void)testGetUserAttributeChangePropertiesWithProfileUpdate { + NSDictionary *profile = @{ + @"name": @"John", + @"age": @"30", + @"cc": @"1234", // Should be skipped + @"tz": @"GMT", // Should be skipped + @"Carrier": @"Jio" // Should be skipped + }; + NSDictionary *event = @{CLTAP_PROFILE: profile}; + + // Mock old values for the keys + id mockOldValueForName = @"Jane"; + id mockOldValueForAge = @"25"; + + // Stub the method to return mock old values + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockGetProfileFieldForKeyName = OCMStub([dataStoreMock getProfileFieldForKey:@"name"]).andReturn(mockOldValueForName); + id mockGetProfileFieldForKeyAge = OCMStub([dataStoreMock getProfileFieldForKey:@"age"]).andReturn(mockOldValueForAge); + + // Call the method and get the result + NSDictionary *result = [dataStoreMock getUserAttributeChangeProperties:event]; + + // Verify the result dictionary + XCTAssertEqual(result.count, 2); + XCTAssertEqual(result[@"name"][CLTAP_KEY_OLD_VALUE], mockOldValueForName); + XCTAssertEqual(result[@"name"][CLTAP_KEY_NEW_VALUE], @"John"); + XCTAssertEqual(result[@"age"][CLTAP_KEY_OLD_VALUE], mockOldValueForAge); + XCTAssertEqual(result[@"age"][CLTAP_KEY_NEW_VALUE], @"30"); + + // Ensure skipped keys are not present in the result + XCTAssertNil(result[@"cc"]); + XCTAssertNil(result[@"tz"]); + XCTAssertNil(result[@"Carrier"]); + + // Verify the mock methods were called + OCMVerify(mockGetProfileFieldForKeyName); + OCMVerify(mockGetProfileFieldForKeyAge); +} + +- (void)testGetUserAttributeChangePropertiesWithIncrementCommand { + NSDictionary *profile = @{ + @"points": @{kCLTAP_COMMAND_INCREMENT: @10} + }; + NSDictionary *event = @{CLTAP_PROFILE: profile}; + + // Mock old and new values for the key + id mockOldValue = @20; + id mockNewValue = @30; + + // Stub the method to return mock old values and handle increment command + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockGetProfileFieldForKey = OCMStub([dataStoreMock getProfileFieldForKey:@"points"]).andReturn(mockOldValue); + + // Call the method and get the result + NSDictionary *result = [dataStoreMock getUserAttributeChangeProperties:event]; + + // Verify the result dictionary + XCTAssertEqual(result.count, 1); + XCTAssertEqual(result[@"points"][CLTAP_KEY_OLD_VALUE], mockOldValue); + XCTAssertEqual(result[@"points"][CLTAP_KEY_NEW_VALUE], mockNewValue); + + // Verify the mock methods were called + OCMVerify(mockGetProfileFieldForKey); +} + +@end diff --git a/CleverTapSDKTests/CTUserInfoMigratorTest.m b/CleverTapSDKTests/CTUserInfoMigratorTest.m new file mode 100644 index 00000000..31e0a5cd --- /dev/null +++ b/CleverTapSDKTests/CTUserInfoMigratorTest.m @@ -0,0 +1,107 @@ +// +// CTUserInfoMigratorTests.m +// CleverTapSDKTests +// +// Created by Kushagra Mishra on 21/06/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTUserInfoMigrator.h" +#import "XCTestCase+XCTestCase_Tests.h" + +@interface CTUserInfoMigratorTest : XCTestCase + +@property (nonatomic, strong) NSFileManager *fileManager; +@property (nonatomic, strong) NSString *libraryPath; + +@end + +@implementation CTUserInfoMigratorTest + + +- (void)setUp { + [super setUp]; + self.fileManager = [NSFileManager defaultManager]; + + // Get the path to the Library directory + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + self.libraryPath = [paths objectAtIndex:0]; +} + +- (void)tearDown { + // Clean up any files created during the test + NSError *error = nil; + NSArray *contents = [self.fileManager contentsOfDirectoryAtPath:self.libraryPath error:&error]; + for (NSString *file in contents) { + if ([file containsString:@"clevertap-"]) { + NSString *filePath = [self.libraryPath stringByAppendingPathComponent:file]; + [self.fileManager removeItemAtPath:filePath error:&error]; + } + } + [super tearDown]; +} + +- (void)testMigrateUserInfoFileForAccountID_WhenOldFileExists_ShouldCopyToNewLocation { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Create the old plist file + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager createFileAtPath:oldFilePath contents:[@"old content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the old file has been copied to the new location + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + XCTAssertTrue([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should exist"); + + // Check that the old file has been deleted + XCTAssertFalse([self.fileManager fileExistsAtPath:oldFilePath], @"Old plist file should be deleted"); +} + +- (void)testMigrateUserInfoFileForAccountID_WhenNewFileExists_ShouldNotCopyAndDeleteOldFile { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Create both old and new plist files + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager createFileAtPath:oldFilePath contents:[@"old content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + [self.fileManager createFileAtPath:newFilePath contents:[@"new content" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the new file still exists + XCTAssertTrue([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should exist"); + + // Check that the old file has been deleted + XCTAssertFalse([self.fileManager fileExistsAtPath:oldFilePath], @"Old plist file should be deleted"); +} + +- (void)testMigrateUserInfoFileForAccountID_WhenOldFileDoesNotExist_ShouldNotCreateNewFile { + NSString *acc_id = @"testAccID"; + NSString *device_id = @"testDeviceID"; + + // Ensure the old plist file does not exist + NSString *oldFileName = [NSString stringWithFormat:@"clevertap-%@-userprofile.plist", acc_id]; + NSString *oldFilePath = [self.libraryPath stringByAppendingPathComponent:oldFileName]; + [self.fileManager removeItemAtPath:oldFilePath error:nil]; + + // Call the method to migrate the user info file + [CTUserInfoMigrator migrateUserInfoFileForAccountID:acc_id deviceID:device_id]; + + // Check that the new file does not exist + NSString *newFileName = [NSString stringWithFormat:@"clevertap-%@-%@-userprofile.plist", acc_id, device_id]; + NSString *newFilePath = [self.libraryPath stringByAppendingPathComponent:newFileName]; + XCTAssertFalse([self.fileManager fileExistsAtPath:newFilePath], @"New plist file should not be created"); +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h b/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h new file mode 100644 index 00000000..ffcc10dd --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadManager+Tests.h @@ -0,0 +1,30 @@ +// +// CTFileDownloadManager+Tests.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTFileDownloadManager_Tests_h +#define CTFileDownloadManager_Tests_h + +#import "CTFileDownloadManager.h" + +@interface CTFileDownloadManager(Tests) + +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSFileManager* fileManager; +@property NSTimeInterval semaphoreTimeout; + +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config; + +- (void)downloadSingleFile:(NSURL *)url +completed:(void(^)(BOOL success))completedBlock; + +- (void)deleteSingleFile:(NSURL *)url + completed:(void(^)(BOOL success))completedBlock; + +@end + +#endif /* CTFileDownloadManager_Tests_h */ diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m b/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m new file mode 100644 index 00000000..2d74fa55 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadManagerTests.m @@ -0,0 +1,605 @@ +#import +#import +#import "CleverTapInstanceConfig.h" +#import "CTFileDownloadManager+Tests.h" +#import "CTConstants.h" +#import "CTFileDownloadTestHelper.h" +#import "NSFileManagerMock.h" +#import "NSURLSessionMock.h" + +@interface CTFileDownloadManagerTests : XCTestCase + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSArray *fileURLs; +@property (nonatomic, strong) CTFileDownloadTestHelper *helper; + +@end + +@implementation CTFileDownloadManagerTests + +- (void)setUp { + [super setUp]; + + self.helper = [CTFileDownloadTestHelper new]; + [self.helper addHTTPStub]; + self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + self.fileDownloadManager = [[CTFileDownloadManager alloc] initWithConfig:self.config]; +} + +- (void)tearDown { + [super tearDown]; + + [self.helper removeStub]; + [self deleteFiles:self.fileURLs]; +} + +- (void)testFilesExist { + [self downloadFiles]; + + for (NSURL *url in self.fileURLs) { + NSString *filePath = [NSString stringWithFormat:@"%lu_%@", [url.absoluteString hash], [url lastPathComponent]]; + NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSString *ctFiles = [documentsDirectory stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + NSString *path = [ctFiles stringByAppendingPathComponent:filePath]; + + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:path]); + } +} + +- (void)testFilePath { + NSURL *url = [NSURL URLWithString:@"https://clevertap.com/ct_test_url_0.png"]; + NSString *filePath = [self.fileDownloadManager filePath:url]; + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + documentsPath = [documentsPath stringByAppendingPathComponent:CLTAP_FILES_DIRECTORY_NAME]; + XCTAssertEqualObjects(filePath, [documentsPath stringByAppendingPathComponent:@"1176188917138815486_ct_test_url_0.png"]); +} + +- (void)testIsFileAlreadyPresent { + [self downloadFiles]; + + for(int i = 0; i < [self.fileURLs count]; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } +} + +- (void)testDeleteFiles { + [self downloadFiles]; + + NSArray *urls = [self.helper generateFileURLs:2]; + [self deleteFiles:urls]; + + // Deleted 1st and 2nd file url. + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[1]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[2]]); +} + +- (void)testDeleteFilesStatus { + [self downloadFiles]; + + NSMutableArray *deleteFileURLs = [NSMutableArray new]; + for(int i = 0; i < self.fileURLs.count; i++) { + // Ensure files are downloaded and saved to disk + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + NSString *fileURL = [self.fileURLs[i] absoluteString]; + [deleteFileURLs addObject:fileURL]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:deleteFileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + for (NSString *url in deleteFileURLs) { + // Assert delete status is success and file is removed from disk + XCTAssertEqual(YES, [status[url] boolValue]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testNotExistDeleteFiles { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + for (NSString *url in urls) { + XCTAssertEqual(YES, [status[url] boolValue]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDeleteSingleFile { + [self downloadFiles:1]; + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete single file"]; + [self.fileDownloadManager deleteSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertEqual(YES, success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[0]]); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDeleteSingleFileIncorrectURL { + XCTestExpectation *expectationNil = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlNil = nil; + [self.fileDownloadManager deleteSingleFile:urlNil completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationNil fulfill]; + }]; + + XCTestExpectation *expectationEmpty = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlEmpty = [NSURL URLWithString:@""]; + [self.fileDownloadManager deleteSingleFile:urlEmpty completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationEmpty fulfill]; + }]; + + XCTestExpectation *expectationNoLastComponent = [self expectationWithDescription:@"Delete single file nil url"]; + NSURL *urlNoLastComponent = [NSURL URLWithString:@"https://no-url-component.png"]; + [self.fileDownloadManager deleteSingleFile:urlNoLastComponent completed:^(BOOL success) { + XCTAssertEqual(NO, success); + [expectationNoLastComponent fulfill]; + }]; + + [self waitForExpectations:@[expectationNil, expectationEmpty, expectationNoLastComponent] timeout:2.0]; +} + +#pragma mark CTFileDownload callback test + +- (void)testAllFilesDownloadedCallback { + self.fileURLs = [self.helper generateFileURLs:2]; + + NSMutableDictionary *expectedStatus = [NSMutableDictionary new]; + NSString *urlString1 = [self.fileURLs[0] absoluteString]; + NSString *urlString2 = [self.fileURLs[1] absoluteString]; + expectedStatus[urlString1] = @1; + expectedStatus[urlString2] = @1; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + + // Assert + void (^completionBlock)(NSDictionary * _Nullable) = ^(NSDictionary * _Nullable status) { + XCTAssertEqualObjects(status, expectedStatus); + [expectation fulfill]; + }; + + // Download files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:completionBlock]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testFileAlreadyDownloaded { + self.fileURLs = [self.helper generateFileURLs:5]; + NSArray *urls = @[self.fileURLs[0], self.fileURLs[1]]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download already present files callback"]; + + // Download 5 files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + + // Assert files are already downloaded + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[0]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + // Call download for 1st and 2nd files again + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation2 fulfill]; + }]; + + [expectation1 fulfill]; + }]; + + // Enforce the order + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + // Ensure total files downloaded are only 5 (one request for each unique file) + XCTAssertEqual(5, self.helper.filesDownloaded.count); + // Ensure 1st and 2nd files are downloaded only once + XCTAssertTrue([self.helper fileDownloadedCount:urls[0]] == 1); + XCTAssertTrue([self.helper fileDownloadedCount:urls[1]] == 1); +} + +- (void)testDownloadsPending { + // Generate 5 file URLs + self.fileURLs = [self.helper generateFileURLs:5]; + NSArray *urls = @[self.fileURLs[0], self.fileURLs[1]]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download 1st and 2nd files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download all files callback"]; + + // Download all 5 files + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation2 fulfill]; + }]; + + // Ensure files are not present yet since download is not yet completed + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:urls[0]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + // Download the 1st and 2nd files + [self.fileDownloadManager downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation1 fulfill]; + }]; + + // Ensure the expecation for Download 1st and 2nd files is fulfilled first + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + // Ensure total files downloaded are only 5 (one request for each unique file) + XCTAssertEqual(5, self.helper.filesDownloaded.count); + // Ensure 1st and 2nd files are downloaded only once + XCTAssertTrue([self.helper fileDownloadedCount:urls[0]] == 1); + XCTAssertTrue([self.helper fileDownloadedCount:urls[1]] == 1); +} + +- (void)testRequestFailure { + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:@"non-existent"]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:[NSData data] + statusCode:404 + headers:@{@"Content-Type":@"text/plain"}]; + }]; + + NSArray *fileURLs = [self.helper generateFileURLs:2]; + NSMutableArray *urls = [fileURLs mutableCopy]; + NSString *nonexistentURLString = @"https://non-existent.com/non-existent.png"; + NSURL *nonexistentURL = [NSURL URLWithString:nonexistentURLString]; + [urls insertObject:nonexistentURL atIndex:0]; + self.fileURLs = urls; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nonnull status) { + // Assert the file that returns 404 has error status + XCTAssertEqualObjects(@0, status[nonexistentURLString]); + // Assert the files that return 200 has success status + XCTAssertEqualObjects(@1, status[[urls[1] absoluteString]]); + XCTAssertEqualObjects(@1, status[[urls[2] absoluteString]]); + + // Assert error file not written to disk + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:nonexistentURL]); + // Assert files written to disk + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[1]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:urls[2]]); + + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testDownloadFilesOneUrlTimeOut { + // Stub the network request for timeout file to simulate a Timed Out Error + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:@"timeout"]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + // Return Timed Out Error and delay the response time with 0.1s + return [[HTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorTimedOut + userInfo:nil]] + responseTime:0.1]; + }]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files with 1 file timed out callback"]; + + NSArray *fileURLs = [self.helper generateFileURLs:2]; + NSMutableArray *fileURLsWithTimedOutURL = [fileURLs mutableCopy]; + [fileURLsWithTimedOutURL insertObject:[NSURL URLWithString:@"https://timeout.com/timeout.png"] atIndex:0]; + self.fileURLs = fileURLsWithTimedOutURL; + + // Download files where 1st file will time out + [self.fileDownloadManager downloadFiles:fileURLsWithTimedOutURL withCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertEqualObjects(@0, status[[fileURLsWithTimedOutURL[0] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLsWithTimedOutURL[1] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLsWithTimedOutURL[2] absoluteString]]); + [expectation2 fulfill]; + }]; + + // Download files + // Ensure the downloadFiles does not wait for the first call to complete + [self.fileDownloadManager downloadFiles:fileURLs withCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertEqualObjects(@1, status[[fileURLs[0] absoluteString]]); + XCTAssertEqualObjects(@1, status[[fileURLs[1] absoluteString]]); + [expectation1 fulfill]; + }]; + + // Ensure 2nd downloadFiles callback does not wait on the 1st + // Ensure the expecation for successful download is called first + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0 enforceOrder:YES]; + [HTTPStubs removeStub:stub]; +} + +- (void)testSemaphoreTimeout { + XCTestExpectation *expectation = [self expectationWithDescription:@"Semaphore Timeout Test"]; + // Generate URLs more than the max concurrency count CLTAP_FILE_MAX_CONCURRENCY_COUNT + self.fileURLs = [self.helper generateFileURLs:15]; + + // Set mock session + NSURLSessionMock *mockSession = [[NSURLSessionMock alloc] init]; + mockSession.delayInterval = 0.3; // Simulate a delay longer than semaphore timeout + self.fileDownloadManager.semaphoreTimeout = 0.1; + self.fileDownloadManager.session = mockSession; + + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nonnull fileDownloadStatus) { + for (NSURL *url in self.fileURLs) { + NSNumber *status = fileDownloadStatus[url.absoluteString]; + XCTAssertNotNil(status, @"File download status should not be nil."); + XCTAssertEqual([status integerValue], 0, @"File download should fail due to semaphore timeout."); + } + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDownloadSingle { + self.fileURLs = [self.helper generateFileURLs:1]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback 1"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files callback 2"]; + + // downloadSingleFile directly downloads the file + [self.fileDownloadManager downloadSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertTrue(success); + [expectation1 fulfill]; + }]; + [self.fileDownloadManager downloadSingleFile:self.fileURLs[0] completed:^(BOOL success) { + XCTAssertTrue(success); + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Expected file requests to equal the calls to downloadSingleFile + XCTAssertTrue([self.helper fileDownloadedCount:self.fileURLs[0]] == 2); +} + +- (void)testDownloadSingleSameURLComponentDifferentHost { + NSURL *url1 = [NSURL URLWithString:[NSString stringWithFormat:@"https://clevertap.com/%@.png", self.helper.fileURL]]; + NSURL *url2 = [NSURL URLWithString:[NSString stringWithFormat:@"https://clevertap-1.com/%@.png", self.helper.fileURL]]; + self.fileURLs = @[url1, url2]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download files callback 1"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download files callback 2"]; + + // Expect to download and save two different files + [self.fileDownloadManager downloadSingleFile:url1 completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url1]); + [expectation1 fulfill]; + }]; + [self.fileDownloadManager downloadSingleFile:url2 completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url2]); + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Expected file requests to equal the calls to downloadSingleFile + XCTAssertEqual(2, self.helper.filesDownloaded.count); + XCTAssertNotEqualObjects([self.fileDownloadManager filePath:url1], [self.fileDownloadManager filePath:url2]); +} + +- (void)testDownloadSingleOverwriteFile { + NSURL *url = [self.helper generateFileURL]; + self.fileURLs = @[url]; + + __block NSDate *firstFileCreationDate; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download file callback"]; + // Download file + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url]); + + // Set the 1st file creation date + NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.fileDownloadManager filePath:url] error:nil]; + firstFileCreationDate = [fileAttributes objectForKey:NSFileCreationDate]; + [expectation1 fulfill]; + }]; + + __block NSDate *secondFileCreationDate; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Download file again callback"]; + // Download the file again. Since the file exists, it should be deleted and then saved. + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertTrue(success); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:url]); + + // Set the 2nd file creation date + NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.fileDownloadManager filePath:url] error:nil]; + secondFileCreationDate = [fileAttributes objectForKey:NSFileCreationDate]; + [expectation2 fulfill]; + }]; + + [self waitForExpectations:@[expectation1, expectation2] timeout:2.0]; + // Ensure the file is overwritten by comparing the created dates + XCTAssertNotEqualObjects(firstFileCreationDate, secondFileCreationDate); +} + +- (void)testDownloadSingleFileWithCreateDirectoryError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.createDirectoryError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testDownloadSingleFileWithFileRemoveError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.fileExists = YES; + fileManagerMock.removeItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testDownloadSingleFileWithFileMoveError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + NSURL *URL = [self.helper generateFileURL]; + fileManagerMock.moveItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + + [self.fileDownloadManager downloadSingleFile:URL completed:^(BOOL success) { + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testTimeoutConfiguration { + NSURLSessionConfiguration *configuration = self.fileDownloadManager.session.configuration; + XCTAssertEqual(configuration.timeoutIntervalForRequest, CLTAP_REQUEST_TIME_OUT_INTERVAL); + XCTAssertEqual(configuration.timeoutIntervalForResource, CLTAP_FILE_RESOURCE_TIME_OUT_INTERVAL); +} + +- (void)testDownloadSingle404 { + // Stub the network request to simulate a 404 Not Found error + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.helper.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:[NSData data] + statusCode:404 + headers:@{@"Content-Type":@"text/plain"}]; + }]; + + NSURL *url = [self.helper generateFileURL]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertFalse(success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:url]); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testDownloadSingleHostNotFound { + id stub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.helper.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + return [HTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotFindHost userInfo:nil]]; + }]; + + NSURL *url = [self.helper generateFileURL]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files callback"]; + [self.fileDownloadManager downloadSingleFile:url completed:^(BOOL success) { + XCTAssertFalse(success); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:url]); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; + [HTTPStubs removeStub:stub]; +} + +- (void)testRemoveAllFiles { + [self downloadFiles:3]; + NSMutableArray *paths = [NSMutableArray array]; + for (int i = 0; i < 3; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + [paths addObject:[self.fileDownloadManager filePath:self.fileURLs[i]]]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Remove all files callback"]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + for (int i = 0; i < 3; i++) { + XCTAssertTrue(status[paths[i]]); + XCTAssertFalse([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testRemoveAllFilesContentsOfDirectoryError { + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion block called"]; + + fileManagerMock.contentsOfDirectoryError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + XCTAssertTrue(status.count == 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + self.fileDownloadManager.fileManager = originalFileManager; +} + +- (void)testRemoveAllFilesRemoveFileError { + [self downloadFiles:3]; + NSMutableArray *paths = [NSMutableArray array]; + for (int i = 0; i < 3; i++) { + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + [paths addObject:[self.fileDownloadManager filePath:self.fileURLs[i]]]; + } + + NSFileManager *originalFileManager = self.fileDownloadManager.fileManager; + NSFileManagerMock *fileManagerMock = [[NSFileManagerMock alloc] init]; + self.fileDownloadManager.fileManager = fileManagerMock; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Remove all files callback"]; + fileManagerMock.removeItemError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:nil]; + [self.fileDownloadManager removeAllFilesWithCompletionBlock:^(NSDictionary * _Nonnull status) { + self.fileDownloadManager.fileManager = originalFileManager; + for (int i = 0; i < 3; i++) { + XCTAssertEqualObjects(@0, status[paths[i]]); + XCTAssertTrue([self.fileDownloadManager isFileAlreadyPresent:self.fileURLs[i]]); + } + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +#pragma mark Private methods + +- (void)downloadFiles { + [self downloadFiles:3]; +} + +- (void)downloadFiles:(int)count { + self.fileURLs = [self.helper generateFileURLs:count]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + + [self.fileDownloadManager downloadFiles:self.fileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)deleteFiles:(NSArray *)urls { + NSMutableArray *deleteFileURLs = [NSMutableArray new]; + for(int i = 0; i < urls.count; i++) { + NSString *fileURL = [urls[i] absoluteString]; + [deleteFileURLs addObject:fileURL]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files"]; + [self.fileDownloadManager deleteFiles:deleteFileURLs withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h new file mode 100644 index 00000000..369e0ccf --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.h @@ -0,0 +1,33 @@ +// +// CTFileDownloadTestHelper.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 22.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CTFileDownloader.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloadTestHelper : NSObject + +@property (nonatomic, strong) NSMutableDictionary *filesDownloaded; + +- (void)addHTTPStub; +- (int)fileDownloadedCount:(NSURL *)url; +- (void)removeStub; + +- (NSString *)fileURL; +- (NSURL *)generateFileURL; +- (NSArray *)generateFileURLs:(int)count; +- (NSString *)generateFileURLString; +- (NSArray *)generateFileURLStrings:(int)count; + +- (void)cleanUpFiles:(CTFileDownloader *)fileDownloader forTest:(XCTestCase *)testCase; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m new file mode 100644 index 00000000..c075cb73 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloadTestHelper.m @@ -0,0 +1,130 @@ +// +// CTFileDownloadTestHelper.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 22.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTFileDownloadTestHelper.h" +#import + +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" +#import +#import "CTPreferences.h" +#import "CTConstants.h" + +@interface CTFileDownloadTestHelper() + +@property (nonatomic, strong) id HTTPStub; +@property (nonatomic, strong) NSArray *fileURLTypes; + +@end + +@implementation CTFileDownloadTestHelper + +- (instancetype)init { + self = [super init]; + if (self) { + self.filesDownloaded = [NSMutableDictionary new]; + self.fileURLTypes = @[@"txt", @"pdf", @"png"]; + } + return self; +} + +- (NSString *)fileURL { + return @"ct_test_url"; +} + +- (int)fileDownloadedCount:(NSURL *)url { + NSString *key = [url absoluteString]; + if (self.filesDownloaded[key]) { + return [self.filesDownloaded[key] intValue]; + } + return -1; +} + +- (void)addHTTPStub { + self.HTTPStub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.absoluteString containsString:self.fileURL]; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + NSString *fileString = [request.URL absoluteString]; + NSString *fileType = [fileString pathExtension]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + + NSNumber *count = self.filesDownloaded[fileString]; + if (count) { + int value = [count intValue]; + count = [NSNumber numberWithInt:value + 1]; + self.filesDownloaded[fileString] = count; + } else { + self.filesDownloaded[fileString] = @1; + } + + if ([fileType isEqualToString:@"txt"]) { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"sampleTXTStub" ofType:@"txt"] + statusCode:200 + headers:nil]; + } else if ([fileType isEqualToString:@"pdf"]) { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"samplePDFStub" ofType:@"pdf"] + statusCode:200 + headers:nil]; + } else { + return [HTTPStubsResponse responseWithFileAtPath:[bundle pathForResource:@"clevertap-logo" ofType:@"png"] + statusCode:200 + headers:nil]; + } + }]; +} + +- (void)removeStub { + if (self.HTTPStub) { + [HTTPStubs removeStub:self.HTTPStub]; + } +} + +- (NSURL *)generateFileURL { + return [NSURL URLWithString:[self generateFileURLStringAtIndex:0]]; +} + +- (NSArray *)generateFileURLs:(int)count { + NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count]; + for (int i = 0; i < count; i++) { + NSString *urlString = [self generateFileURLStringAtIndex:i]; + [arr addObject:[NSURL URLWithString:urlString]]; + } + return arr; +} + +- (NSString *)generateFileURLString { + return [self generateFileURLStrings:1][0]; +} + +- (NSArray *)generateFileURLStrings:(int)count { + NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count]; + for (int i = 0; i < count; i++) { + NSString *urlString = [self generateFileURLStringAtIndex:i]; + [arr addObject:urlString]; + } + return arr; +} + +- (NSString *)generateFileURLStringAtIndex:(int)index { + int type = index >= 3 ? index % 3 : index; + return [NSString stringWithFormat:@"https://clevertap.com/%@_%d.%@", self.fileURL, index, self.fileURLTypes[type]]; +} + +- (void)cleanUpFiles:(CTFileDownloader *)fileDownloader forTest:(XCTestCase *)testCase { + [CTPreferences removeObjectForKey:[fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + [CTPreferences removeObjectForKey:[fileDownloader storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; + + XCTestExpectation *expectation = [testCase expectationWithDescription:@"Wait for remove all assets"]; + // Clear all files + [fileDownloader removeAllAssetsWithCompletion:^(NSDictionary *status) { + [expectation fulfill]; + }]; + [testCase waitForExpectations:@[expectation] timeout:2.0]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h b/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h new file mode 100644 index 00000000..f2004b83 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloader+Tests.h @@ -0,0 +1,33 @@ +// +// CTFileDownloader+Tests.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTFileDownloader_Tests_h +#define CTFileDownloader_Tests_h + +#import "CTFileDownloader.h" + +@interface CTFileDownloader(Tests) + +@property (nonatomic, strong) CTFileDownloadManager *fileDownloadManager; +@property (nonatomic, strong) NSMutableDictionary *urlsExpiry; +@property (nonatomic) NSTimeInterval fileExpiryTime; +- (long)currentTimeInterval; +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime; +- (void)removeDeletedFilesFromExpiry:(NSDictionary *)status; +- (void)updateFilesExpiryInPreference; +- (void)updateLastDeletedTimestamp; +- (long)lastDeletedTimestamp; +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion; +- (void)removeLegacyAssets:(void (^)(void))completion; +- (NSString *)storageKeyWithSuffix:(NSString *)suffix; +- (void)updateFilesExpiry:(NSDictionary *)status; +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion; + +@end + +#endif /* CTFileDownloader_Tests_h */ diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h new file mode 100644 index 00000000..dff2661b --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.h @@ -0,0 +1,29 @@ +// +// CTFileDownloaderMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTFileDownloader.h" +#import "CTFileDownloadManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloaderMock : CTFileDownloader + +@property (nonatomic) long mockCurrentTimeInterval; +@property (nonatomic) void(^removeInactiveExpiredAssetsBlock)(long); + +@property (nonatomic) CTFilesDeleteCompletedBlock deleteCompletion; +@property (nonatomic, nullable) void(^deleteFilesInvokedBlock)(NSArray *); + +@property (nonatomic, nullable) CTFilesDeleteCompletedBlock removeAllAssetsCompletion; + +@property (nonatomic) CTFilesDownloadCompletedBlock downloadCompletion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m new file mode 100644 index 00000000..ce0498f5 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderMock.m @@ -0,0 +1,67 @@ +// +// CTFileDownloaderMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 23.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" + +@implementation CTFileDownloaderMock + +- (long)currentTimeInterval { + if (self.mockCurrentTimeInterval) { + return self.mockCurrentTimeInterval; + } + return [super currentTimeInterval]; +} + +- (void)removeInactiveExpiredAssets:(long)lastDeletedTime { + if (self.removeInactiveExpiredAssetsBlock) { + self.removeInactiveExpiredAssetsBlock(lastDeletedTime); + } + [super removeInactiveExpiredAssets:lastDeletedTime]; +} + +- (void)deleteFiles:(NSArray *)urls withCompletionBlock:(CTFilesDeleteCompletedBlock)completion { + if (self.deleteFilesInvokedBlock) { + self.deleteFilesInvokedBlock(urls); + } + CTFilesDeleteCompletedBlock completionBlock = ^(NSDictionary *status) { + if (completion) { + completion(status); + } + if (self.deleteCompletion) { + self.deleteCompletion(status); + } + }; + [super deleteFiles:urls withCompletionBlock:completionBlock]; +} + +- (void)removeAllAssetsWithCompletion:(void(^)(NSDictionary *status))completion { + CTFilesDeleteCompletedBlock completionBlock = ^(NSDictionary *status) { + if (completion) { + completion(status); + } + if (self.removeAllAssetsCompletion) { + self.removeAllAssetsCompletion(status); + } + }; + [super removeAllAssetsWithCompletion:completionBlock]; +} + +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^)(NSDictionary * _Nonnull))completion { + CTFilesDownloadCompletedBlock completionBlock = ^(NSDictionary *status) { + if (completion) { + completion(status); + } + if (self.downloadCompletion) { + self.downloadCompletion(status); + } + }; + [super downloadFiles:fileURLs withCompletionBlock:completionBlock]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m b/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m new file mode 100644 index 00000000..3d714d18 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/CTFileDownloaderTests.m @@ -0,0 +1,531 @@ +#import +#import +#import "CTPreferences.h" +#import "CTConstants.h" +#import "CTFileDownloadManager.h" +#import "CTFileDownloadTestHelper.h" +#import "CTFileDownloader+Tests.h" +#import "CTFileDownloaderMock.h" +#import +#import + +@interface CTFileDownloaderTests : XCTestCase + +@property (nonatomic, strong) CleverTapInstanceConfig *config; +@property (nonatomic, strong) CTFileDownloaderMock *fileDownloader; +@property (nonatomic, strong) NSArray *fileURLs; +@property (nonatomic, strong) CTFileDownloadTestHelper *helper; + +@end + +@implementation CTFileDownloaderTests + +- (void)setUp { + [super setUp]; + + self.helper = [CTFileDownloadTestHelper new]; + [self.helper addHTTPStub]; + + self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; +} + +- (void)tearDown { + [super tearDown]; + + [self.helper removeStub]; + + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_ASSETS_LAST_DELETED_TS]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for cleanup"]; + self.fileDownloader.removeAllAssetsCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + // Clear all files + [self.fileDownloader clearFileAssets:NO]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testRemoveLegacyAssets { + // Setup + NSArray *urls = @[ + [NSString stringWithFormat:@"https://clevertap.com/%@_0.png", self.helper.fileURL], + [NSString stringWithFormat:@"https://clevertap.com/%@_1.jpg", self.helper.fileURL], + [NSString stringWithFormat:@"https://clevertap.com/%@_2.jpeg", self.helper.fileURL], + [NSString stringWithFormat:@"https://clevertap.com/%@_3.png", self.helper.fileURL] + ]; + NSArray *activeAssetsArray = @[urls[0], urls[1]]; + NSArray *inactiveAssetsArray = @[urls[2], urls[3]]; + [CTPreferences putObject:activeAssetsArray forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]; + [CTPreferences putObject:inactiveAssetsArray forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]; + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + [CTPreferences putInt:ts forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; + + SDWebImageManager *sdWebImageManager = [SDWebImageManager sharedManager]; + XCTestExpectation *expectation = [self expectationWithDescription:@"SDWebImage loadImageWithURL"]; + dispatch_group_t downloads = dispatch_group_create(); + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + for (NSString *url in urls) { + dispatch_group_enter(downloads); + dispatch_async(concurrentQueue, ^{ + [sdWebImageManager loadImageWithURL:[NSURL URLWithString:url] + options:SDWebImageRetryFailed + context:@{SDWebImageContextStoreCacheType : @(SDImageCacheTypeDisk)} + progress:nil + completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + dispatch_group_leave(downloads); + }]; + }); + } + dispatch_group_notify(downloads, concurrentQueue, ^{ + [expectation fulfill]; + }); + + [self waitForExpectations:@[expectation] timeout:2.0]; + SDImageCache *sdImageCache = [SDImageCache sharedImageCache]; + for (NSString *url in urls) { + XCTAssertNotNil([sdImageCache imageFromDiskCacheForKey:url]); + } + + // Remove legacy assets + XCTestExpectation *expectationRemoveLegacyAssets = [self expectationWithDescription:@"RemoveLegacyAssets"]; + [self.fileDownloader removeLegacyAssets:^{ + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ACTIVE_ASSETS]]); + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_INACTIVE_ASSETS]]); + XCTAssertNil([CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]); + for (NSString *url in urls) { + XCTAssertNil([sdImageCache imageFromDiskCacheForKey:url]); + } + [expectationRemoveLegacyAssets fulfill]; + }]; + + [self waitForExpectations:@[expectationRemoveLegacyAssets] timeout:2.0]; +} + +- (void)testSetup { + // Test setup initializes the FileDownloader with the urlsExpiry from cache + NSDictionary *urlsExpiry= @{ + @"url0": @(1), + @"url1": @(1) + }; + [CTPreferences putObject:urlsExpiry forKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; + XCTAssertTrue([urlsExpiry isEqualToDictionary:self.fileDownloader.urlsExpiry]); + XCTAssertTrue([self.fileDownloader.urlsExpiry isKindOfClass:[NSMutableDictionary class]]); + + // Test setup initializes the FileDownloader with empty dictionary if no cached value + [CTPreferences removeObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]; + self.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:self.config]; + XCTAssertNotNil(self.fileDownloader.urlsExpiry); + XCTAssertEqual(0, self.fileDownloader.urlsExpiry.count); + XCTAssertTrue([self.fileDownloader.urlsExpiry isKindOfClass:[NSMutableDictionary class]]); +} + +- (void)testDefaultExpiryTime { + XCTAssertEqual(self.fileDownloader.fileExpiryTime, CLTAP_FILE_EXPIRY_OFFSET); +} + +- (void)testFileAlreadyPresent { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[0] andUpdateExpiryTime:NO]); + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[1] andUpdateExpiryTime:NO]); + + [self downloadFiles:@[urls[0]]]; + + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0] andUpdateExpiryTime:NO]); + XCTAssertFalse([self.fileDownloader isFileAlreadyPresent:urls[1] andUpdateExpiryTime:NO]); +} + +- (void)testImageLoadedFromDisk { + // Download files + NSArray *urls = [self.helper generateFileURLStrings:3]; + // Download files, urls[2] is of type txt + [self downloadFiles:@[urls[2]]]; + + // Check image is present in disk cache + UIImage *image = [self.fileDownloader loadImageFromDisk:urls[2]]; + XCTAssertNotNil(image); +} + +- (void)testImageNotLoadedFromDisk { + NSArray *urls = [self.helper generateFileURLStrings:3]; + // Download files, urls[0] is of type txt + [self downloadFiles:@[urls[0]]]; + + // Check image is present in disk cache + UIImage *image = [self.fileDownloader loadImageFromDisk:urls[0]]; + XCTAssertNil(image); +} + +- (void)testFileDownloadPath { + NSArray *urls = [self.helper generateFileURLStrings:1]; + [self downloadFiles:urls]; + NSString *filePath = [self.fileDownloader fileDownloadPath:urls[0]]; + + NSURL *URL = [NSURL URLWithString:urls[0]]; + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSString *pathComponent = [URL lastPathComponent]; + long hash = [urls[0] hash]; + NSString *fileName = [NSString stringWithFormat:@"%@/%ld_%@", CLTAP_FILES_DIRECTORY_NAME, hash, pathComponent]; + NSString *expectedFilePath = [documentsPath stringByAppendingPathComponent:fileName]; + XCTAssertNotNil(filePath); + XCTAssertEqualObjects(filePath, expectedFilePath); +} + +- (void)testFileDownloadPathNotFound { + NSArray *urls = [self.helper generateFileURLStrings:1]; + NSString *filePath = [self.fileDownloader fileDownloadPath:urls[0]]; + XCTAssertNil(filePath); +} + +- (void)testDownloadEmptyUrls { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + [self.fileDownloader downloadFiles:@[] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertNotNil(status); + XCTAssertTrue(status.count == 0); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testDownloadUpdatesFileExpiryTs { + // Mock currentTimeInterval + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *url = [self.helper generateFileURLStrings:1][0]; + // Ensure not yet present + XCTAssertNil(self.fileDownloader.urlsExpiry[url]); + + [self downloadFiles:@[url]]; + long expiryDate = ts + self.fileDownloader.fileExpiryTime; + // Ensure url has correct expiry set + XCTAssertEqualObjects(@(expiryDate), self.fileDownloader.urlsExpiry[url]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self downloadFiles:@[url]]; + // Ensure url expiry is updated + XCTAssertEqualObjects(@(expiryDate + 100), self.fileDownloader.urlsExpiry[url]); +} + +- (void)testDownloadUpdatesFileExpiryCache { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTAssertEqual(0, self.fileDownloader.urlsExpiry.count); + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + [self downloadFiles:urls]; + NSDictionary *urlsExpiry = [NSDictionary dictionaryWithDictionary:self.fileDownloader.urlsExpiry]; + XCTAssertEqual(2, self.fileDownloader.urlsExpiry.count); + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self downloadFiles:urls]; + XCTAssertNotEqualObjects(urlsExpiry, self.fileDownloader.urlsExpiry); + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); +} + +- (void)testDownloadTriggersRemoveExpired { + NSArray *urls = [self.helper generateFileURLStrings:2]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Download triggers remove expired files"]; + long lastDeletedTs = [self.fileDownloader lastDeletedTimestamp]; + // Download files should trigger removeInactiveExpiredAssets with the lastDeletedTimestamp + self.fileDownloader.removeInactiveExpiredAssetsBlock = ^(long lastDeleted) { + XCTAssertEqual(lastDeletedTs, lastDeleted); + [expectation fulfill]; + }; + [self downloadFiles:urls]; + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testRemoveExpiredAssetsNoDeletedFiles { + // This block is synchronous + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + // Delete files should not be invoked if the last deleted time is within the expiry time + XCTFail(); + }; + + long ts = (long)[[NSDate date] timeIntervalSince1970]; + long lastDeleted = ts - 1; + self.fileDownloader.mockCurrentTimeInterval = ts; + self.fileDownloader.urlsExpiry = [@{ + @"url0": @(4) + } mutableCopy]; + [self.fileDownloader removeInactiveExpiredAssets:lastDeleted]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testRemoveExpiredAssets { + // Mock the current time + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *expiredUrl1 = @"url0-expired"; + NSString *expiredUrl2 = @"url2-expired"; + + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + // Delete files is invoked with the expired urls only + NSSet *urlsSet = [NSSet setWithArray:urls]; + NSSet *expected = [NSSet setWithArray:@[expiredUrl1, expiredUrl2]]; + XCTAssertEqualObjects(expected, urlsSet); + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for delete completion"]; + __weak CTFileDownloaderTests *weakSelf = self; + // Expired files are deleted + self.fileDownloader.deleteCompletion = ^(NSDictionary * _Nonnull status) { + XCTAssertNotNil([status objectForKey:expiredUrl1]); + XCTAssertNotNil([status objectForKey:expiredUrl2]); + + NSDictionary *urlsExpiry = [@{ + @"url1": @(ts), + @"url3": @(ts + 1) + } mutableCopy]; + + XCTAssertTrue([weakSelf.fileDownloader.urlsExpiry isEqualToDictionary:urlsExpiry]); + [expectation fulfill]; + }; + + // Calculate last deleted to force remove assets + long lastDeleted = ts - self.fileDownloader.fileExpiryTime - 1; + + // Set the urls expiry to have both expired and valid assets + self.fileDownloader.urlsExpiry = [@{ + expiredUrl1: @(ts - 1), + @"url1": @(ts), + expiredUrl2: @(ts - 60), + @"url3": @(ts + 1), + } mutableCopy]; + + [self.fileDownloader removeInactiveExpiredAssets:lastDeleted]; + [self waitForExpectations:@[expectation] timeout:2.0]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testUpdateFilesExpiry { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + long expiry = ts + self.fileDownloader.fileExpiryTime; + + // url0 and url3 are successful + NSDictionary *status = @{ + @"url0": @(1), + @"url1": @(0), + @"url2": @(0), + @"url3": @(1) + }; + + long previousExpiry = (ts - 100) + self.fileDownloader.fileExpiryTime; + // url3 is not in the expiry dictionary + self.fileDownloader.urlsExpiry = [@{ + @"url0": @(previousExpiry), + @"url2": @(previousExpiry), + @"url4": @(previousExpiry) + } mutableCopy]; + + [self.fileDownloader updateFilesExpiry:status]; + + // Expect url0 to be updated and url3 to be added + NSMutableDictionary *expected = [@{ + @"url0": @(expiry), + @"url2": @(previousExpiry), + @"url3": @(expiry), + @"url4": @(previousExpiry) + } mutableCopy]; + + XCTAssertEqualObjects(expected, self.fileDownloader.urlsExpiry); +} + +- (void)testDeleteFiles { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + // Set 3 files in urlsExpiry + NSArray *urls = [self.helper generateFileURLStrings:3]; + for (NSString *url in urls) { + self.fileDownloader.urlsExpiry[url] = @(ts); + } + + // Assert lastDeletedTimestamp returns current timestamp + XCTAssertEqual(ts, [self.fileDownloader lastDeletedTimestamp]); + // Change the current timestamp + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete files."]; + // Delete 1st and 2nd file (3rd file is not deleted) + [self.fileDownloader deleteFiles:@[urls[0], urls[1]] withCompletionBlock:^(NSDictionary * _Nonnull status) { + // Assert files are deleted + XCTAssertEqualObjects(status[urls[0]], @1); + XCTAssertEqualObjects(status[urls[1]], @1); + + // Assert 1st and 2nd files are removed from urlsExpiry + // Assert 3rd file is still in the urlsExpiry + NSDictionary *expectedExpiry = [@{ + urls[2]: @(ts) + } mutableCopy]; + XCTAssertTrue([expectedExpiry isEqualToDictionary:self.fileDownloader.urlsExpiry]); + + // Assert expiry is updated in preferences + XCTAssertEqualObjects(self.fileDownloader.urlsExpiry, [CTPreferences getObjectForKey:[self.fileDownloader storageKeyWithSuffix:CLTAP_FILE_URLS_EXPIRY_DICT]]); + + // Assert last deleted timestamp is updated + XCTAssertEqual(ts + 100, [self.fileDownloader lastDeletedTimestamp]); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testClearExpiredFiles { + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + NSArray *urlsExpiry = [self.helper generateFileURLStrings:3]; + for (int i = 1; i < urlsExpiry.count; i++) { + // Set to expired + self.fileDownloader.urlsExpiry[urlsExpiry[i]] = @(ts - 1); + } + // Set non expired + self.fileDownloader.urlsExpiry[urlsExpiry[0]] = @(ts); + XCTestExpectation *expectation = [self expectationWithDescription:@"ClearAllFiles expired only triggers remove expired files"]; + self.fileDownloader.removeInactiveExpiredAssetsBlock = ^(long lastDeleted) { + long expectedForceLastDeleted = (ts - self.fileDownloader.fileExpiryTime) - 1; + XCTAssertEqual(expectedForceLastDeleted, lastDeleted); + XCTAssertTrue(ts - expectedForceLastDeleted > self.fileDownloader.fileExpiryTime); + [expectation fulfill]; + }; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"ClearAllFiles trigger delete files"]; + self.fileDownloader.deleteFilesInvokedBlock = ^(NSArray *urls) { + NSSet *urlsSet = [NSSet setWithArray:urls]; + // Expired URLs are to be deleted + NSSet *expected = [NSSet setWithObjects:urlsExpiry[1], urlsExpiry[2], nil]; + XCTAssertEqualObjects(expected, urlsSet); + [expectation2 fulfill]; + }; + [self.fileDownloader clearFileAssets:YES]; + [self waitForExpectations:@[expectation, expectation2] timeout:2.0]; + self.fileDownloader.deleteFilesInvokedBlock = nil; +} + +- (void)testDownloadAndClearAllFileAssets { + // Mock the current timestamp + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + // Download files + NSArray *urls = [self.helper generateFileURLStrings:3]; + [self downloadFiles:urls]; + // Assert expiry is updated + XCTAssertEqual(3, self.fileDownloader.urlsExpiry.count); + + NSMutableArray *paths = [NSMutableArray array]; + for (NSString *url in urls) { + // Assert the files are downloaded + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:url andUpdateExpiryTime:NO]); + [paths addObject:[self.fileDownloader fileDownloadPath:url]]; + } + + long lastDeleted = self.fileDownloader.lastDeletedTimestamp; + self.fileDownloader.mockCurrentTimeInterval = lastDeleted + 100; + + // Clear all file assets + XCTestExpectation *expectation = [self expectationWithDescription:@"Clear all assets"]; + __weak CTFileDownloaderTests *weakSelf = self; + self.fileDownloader.removeAllAssetsCompletion = ^(NSDictionary * _Nonnull status) { + // Assert all files status is success + for (NSString *path in paths) { + XCTAssertTrue(status[path]); + } + // Assert the files no longer exist + for (NSString *url in urls) { + XCTAssertFalse([weakSelf.fileDownloader isFileAlreadyPresent:url andUpdateExpiryTime:NO]); + } + // Assert urlsExpiry is cleared + XCTAssertEqual(0, weakSelf.fileDownloader.urlsExpiry.count); + // Assert the last deleted ts is updated + XCTAssertEqual(lastDeleted + 100, weakSelf.fileDownloader.lastDeletedTimestamp); + [expectation fulfill]; + }; + + [self.fileDownloader clearFileAssets:NO]; + [self waitForExpectations:@[expectation] timeout:2.0]; + self.fileDownloader.removeAllAssetsCompletion = nil; +} + +- (void)testFileDownloadCallbacksWhenFileIsAlreadyDownloading { + // This test checks the file download callbacks when same url is already downloading. + // Verified from adding logs that same url is not downloaded twice if download is in + // progress for that url. Also callbacks are triggered when download is completed. + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Wait for first download callbacks"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Wait for second download callbacks"]; + + NSArray *urls = [self.helper generateFileURLStrings:3]; + [self.fileDownloader downloadFiles:@[urls[0], urls[1], urls[2]] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0] andUpdateExpiryTime:NO]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[1] andUpdateExpiryTime:NO]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[2] andUpdateExpiryTime:NO]); + [expectation1 fulfill]; + }]; + [self.fileDownloader downloadFiles:@[urls[0], urls[1]] withCompletionBlock:^(NSDictionary * _Nullable status) { + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[0] andUpdateExpiryTime:NO]); + XCTAssertTrue([self.fileDownloader isFileAlreadyPresent:urls[1] andUpdateExpiryTime:NO]); + [expectation2 fulfill]; + }]; + [self waitForExpectations:@[expectation2, expectation1] timeout:2.0 enforceOrder:YES]; +} + +- (void)testFileAlreadyPresentUpdatesFileExpiryTime { + // This test checks that file expiry time is updated when file is already present + // and `isFileAlreadyPresent:` method is called with andUpdateExpiryTime YES. + + // Mock currentTimeInterval + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *url = [self.helper generateFileURLStrings:1][0]; + [self downloadFiles:@[url]]; + long expiryDate = ts + self.fileDownloader.fileExpiryTime; + // Ensure url has correct expiry set + XCTAssertEqualObjects(@(expiryDate), self.fileDownloader.urlsExpiry[url]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self.fileDownloader isFileAlreadyPresent:url andUpdateExpiryTime:YES]; + // Ensure url expiry is updated + XCTAssertEqualObjects(@(expiryDate + 100), self.fileDownloader.urlsExpiry[url]); +} + +- (void)testFileAlreadyPresentDoesNotUpdatesFileExpiryTime { + // This test checks that file expiry time is not updated when file is already present + // and `isFileAlreadyPresent:` method is called with andUpdateExpiryTime NO. + + // Mock currentTimeInterval + long ts = (long)[[NSDate date] timeIntervalSince1970]; + self.fileDownloader.mockCurrentTimeInterval = ts; + + NSString *url = [self.helper generateFileURLStrings:1][0]; + [self downloadFiles:@[url]]; + long expiryDate = ts + self.fileDownloader.fileExpiryTime; + // Ensure url has correct expiry set + XCTAssertEqualObjects(@(expiryDate), self.fileDownloader.urlsExpiry[url]); + + self.fileDownloader.mockCurrentTimeInterval = ts + 100; + [self.fileDownloader isFileAlreadyPresent:url andUpdateExpiryTime:NO]; + // Ensure url expiry is not updated + XCTAssertEqualObjects(@(expiryDate), self.fileDownloader.urlsExpiry[url]); +} + +#pragma mark Private methods + +- (void)downloadFiles:(NSArray *)urls { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download files"]; + [self.fileDownloader downloadFiles:urls withCompletionBlock:^(NSDictionary * _Nullable status) { + [expectation fulfill]; + }]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/NSFileManagerMock.h b/CleverTapSDKTests/FileDownload/NSFileManagerMock.h new file mode 100644 index 00000000..54b6a713 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSFileManagerMock.h @@ -0,0 +1,23 @@ +// +// NSFileManagerMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 24.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSFileManagerMock : NSFileManager + +@property (nonatomic, strong) NSError *createDirectoryError; +@property (nonatomic, strong) NSError *removeItemError; +@property (nonatomic, strong) NSError *moveItemError; +@property (nonatomic, assign) BOOL fileExists; +@property (nonatomic, strong) NSError *contentsOfDirectoryError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/NSFileManagerMock.m b/CleverTapSDKTests/FileDownload/NSFileManagerMock.m new file mode 100644 index 00000000..072e13a2 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSFileManagerMock.m @@ -0,0 +1,50 @@ +// +// NSFileManagerMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 24.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "NSFileManagerMock.h" + +@implementation NSFileManagerMock + +- (BOOL)fileExistsAtPath:(NSString *)path { + return self.fileExists; +} + +- (BOOL)createDirectoryAtURL:(NSURL *)url withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error { + if (self.createDirectoryError) { + *error = self.createDirectoryError; + return NO; + } + return YES; +} + +- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error { + if (self.removeItemError) { + *error = self.removeItemError; + return NO; + } + return YES; +} + +- (BOOL)moveItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL error:(NSError **)error { + if (self.moveItemError) { + *error = self.moveItemError; + return NO; + } + return YES; +} + +- (NSArray *)contentsOfDirectoryAtURL:(NSURL *)url includingPropertiesForKeys:(NSArray *)keys options:(NSDirectoryEnumerationOptions)mask error:(NSError *__autoreleasing _Nullable *)error { + if (self.contentsOfDirectoryError) { + *error = self.contentsOfDirectoryError; + return nil; + } + + return [super contentsOfDirectoryAtURL:url includingPropertiesForKeys:keys options:mask error:error]; +} + +@end diff --git a/CleverTapSDKTests/FileDownload/NSURLSessionMock.h b/CleverTapSDKTests/FileDownload/NSURLSessionMock.h new file mode 100644 index 00000000..f0818770 --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSURLSessionMock.h @@ -0,0 +1,29 @@ +// +// NSURLSessionMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 30.07.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSURLSessionMock : NSURLSession + +@property (nonatomic, assign) NSTimeInterval delayInterval; + +@end + +@interface NSURLSessionDownloadTaskMock : NSURLSessionDownloadTask + +@property (nonatomic, copy) void (^completionHandler)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable); +@property (nonatomic, assign) NSTimeInterval delayInterval; + +- (instancetype)initWithCompletionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler + delayInterval:(NSTimeInterval)delayInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/FileDownload/NSURLSessionMock.m b/CleverTapSDKTests/FileDownload/NSURLSessionMock.m new file mode 100644 index 00000000..43f41d4a --- /dev/null +++ b/CleverTapSDKTests/FileDownload/NSURLSessionMock.m @@ -0,0 +1,41 @@ +// +// NSURLSessionMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 30.07.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "NSURLSessionMock.h" + +@implementation NSURLSessionMock + +- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable))completionHandler { + return [[NSURLSessionDownloadTaskMock alloc] initWithCompletionHandler:completionHandler delayInterval:self.delayInterval]; +} + +@end + +@implementation NSURLSessionDownloadTaskMock + +- (instancetype)initWithCompletionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler + delayInterval:(NSTimeInterval)delayInterval { + self = [super init]; + if (self) { + _completionHandler = [completionHandler copy]; + _delayInterval = delayInterval; + } + return self; +} + +- (void)resume { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delayInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.completionHandler) { + self.completionHandler(nil, nil, [NSError errorWithDomain:@"MockErrorDomain" code:-1001 userInfo:@{NSLocalizedDescriptionKey: @"Timeout"}]); + } + }); +} + +@end + + diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h b/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h index 3c305743..1ce0cf2f 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManager+Tests.h @@ -14,6 +14,8 @@ @property (nonatomic, strong) CTInAppDisplayManager *inAppDisplayManager; @property (nonatomic, strong) NSMutableArray *suppressedClientSideInApps; @property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIds; +@property (nonatomic, strong) NSMutableArray *evaluatedServerSideInAppIdsForProfile; +@property (nonatomic, strong) NSMutableArray *suppressedClientSideInAppsForProfile; @property (nonatomic, strong) NSDictionary *appLaunchedProperties; - (void)sortByPriority:(NSMutableArray *)inApps; - (NSMutableArray *)evaluate:(CTEventAdapter *)event withInApps:(NSArray *)inApps; @@ -24,6 +26,8 @@ - (void)onAppLaunchedWithSuccess:(BOOL)success; - (void)saveEvaluatedServerSideInAppIds; - (void)saveSuppressedClientSideInApps; +- (void)saveEvaluatedServerSideInAppIdsForProfile; +- (void)saveSuppressedClientSideInAppsForProfile; - (NSString *)storageKeyWithSuffix:(NSString *)suffix; @end diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m index cea7a903..e66f4280 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m @@ -8,6 +8,7 @@ #import #import +#import "CTTemplatePresenterMock.h" #import "CTInAppEvaluationManager.h" #import "CTEventAdapter.h" #import "BaseTestCase.h" @@ -21,6 +22,9 @@ #import "CTInAppEvaluationManager+Tests.h" #import "CTPreferences.h" #import "CTMultiDelegateManager+Tests.h" +#import "CTCustomTemplatesManager-Internal.h" +#import "CTInAppTemplateBuilder.h" +#import "CTTestTemplateProducer.h" @interface CTInAppDisplayManagerMock : CTInAppDisplayManager @property (nonatomic, strong) NSMutableArray *inappNotifs; @@ -31,12 +35,25 @@ - (void)_addInAppNotificationsToQueue:(NSArray *)inappNotifs; #pragma clang diagnostic ignored "-Wnonnull" @implementation CTInAppDisplayManagerMock - (instancetype)initWithNil { - if (self = [super initWithCleverTap:nil + if (self = [super initWithCleverTap:nil dispatchQueueManager:nil inAppFCManager:nil impressionManager:nil inAppStore:nil - imagePrefetchManager:nil]) { + templatesManager:nil + fileDownloader:nil]) { + self.inappNotifs = [NSMutableArray new]; + } + return self; +} +- (instancetype)initWithTemplateManager:(CTCustomTemplatesManager *)templatesManager { + if (self = [super initWithCleverTap:nil + dispatchQueueManager:nil + inAppFCManager:nil + impressionManager:nil + inAppStore:nil + templatesManager:templatesManager + fileDownloader:nil]) { self.inappNotifs = [NSMutableArray new]; } return self; @@ -66,15 +83,20 @@ - (void)setUp { } - (void)tearDown { - // Clean up resources if needed - //self.evaluationManager = nil; + // Remove triggers for (int i = 1; i <= 4; i++) { [self.evaluationManager.triggerManager removeTriggers:[NSString stringWithFormat:@"%d", i]]; } + // Remove saved ids self.evaluationManager.evaluatedServerSideInAppIds = [NSMutableArray new]; [self.evaluationManager saveEvaluatedServerSideInAppIds]; self.evaluationManager.suppressedClientSideInApps = [NSMutableArray new]; [self.evaluationManager saveSuppressedClientSideInApps]; + + self.evaluationManager.evaluatedServerSideInAppIdsForProfile = [NSMutableArray new]; + [self.evaluationManager saveEvaluatedServerSideInAppIdsForProfile]; + self.evaluationManager.suppressedClientSideInAppsForProfile = [NSMutableArray new]; + [self.evaluationManager saveSuppressedClientSideInAppsForProfile]; [super tearDown]; } @@ -299,6 +321,43 @@ - (void)testEvaluateWithInApps { XCTAssertEqual([self.evaluationManager.triggerManager getTriggers:@"4"], 1); } +- (void)testEvaluateUserAttribute { + + self.helper.inAppStore.serverSideInApps = @[ + @{ + @"ti": @1, + @"whenTriggers": @[@{ + @"eventProperties": @[@{ + @"propertyName": @"newValue", + @"propertyValue": @"Gold", + }], + @"profileAttrName": @"Customer Type", + }] + }, + @{ + @"ti": @2, + @"whenTriggers": @[@{ + @"eventProperties": @[@{ + @"propertyName": @"newValue", + @"propertyValue": @"Premium", + }], + @"profileAttrName": @"Customer Type", + }] + }, + ]; + NSDictionary *profile = @{ + @"Customer Type": @{ + @"newValue": @"Gold", + @"oldValue": @"Premium" + } + }; + + + [self.evaluationManager evaluateOnUserAttributeChange:profile]; + XCTAssertEqualObjects((@[@1]), self.evaluationManager.evaluatedServerSideInAppIdsForProfile); + XCTAssertNotEqualObjects((@[@2]), self.evaluationManager.evaluatedServerSideInAppIdsForProfile); +} + - (void)testEvaluateCharged { self.helper.inAppStore.serverSideInApps = @[ @{ @@ -401,7 +460,7 @@ - (void)testEvaluationManagerCaching { CLTAP_INAPP_SUPPRESSED_META_KEY: @[@0] } ]; - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@3]), [self savedEvaluatedServerSideInAppIds]); XCTAssertEqual(1, [[self savedSuppressedClientSideInApps] count]); @@ -610,6 +669,50 @@ - (void)testEvaluateOnAppLaunchedServerSide { XCTAssertEqualObjects((@[inApps[0]]), self.mockDisplayManager.inappNotifs); } +- (void)testEvaluateCustomInApps { + NSMutableSet *templates = [NSMutableSet set]; + CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; + CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; + [templateBuilder setName:@"Template 1"]; + [templateBuilder setPresenter:templatePresenter]; + [templates addObject:[templateBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + + [CTCustomTemplatesManager registerTemplateProducer:producer]; + + CTCustomTemplatesManager *templatesManager = [[CTCustomTemplatesManager alloc] initWithConfig:self.helper.config]; + + // Initialize with the templatesManager to register the template + self.mockDisplayManager = [[CTInAppDisplayManagerMock alloc] initWithTemplateManager:templatesManager]; + self.evaluationManager.inAppDisplayManager = self.mockDisplayManager; + + NSArray *inApps = @[ + @{ + @"ti": @1, + @"templateName": @"Template 2", + @"type": @"custom-code", + @"priority": @(100), + @"whenTriggers": @[@{ + @"eventName": @"event1" + }] + }, + @{ + @"ti": @2, + @"templateName": @"Template 1", + @"type": @"custom-code", + @"priority": @(100), + @"whenTriggers": @[@{ + @"eventName": @"event1" + }] + } + ]; + + CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:@"event1" eventProperties:@{} andLocation:kCLLocationCoordinate2DInvalid]; + + XCTAssertEqualObjects([self.evaluationManager evaluate:event withInApps:inApps], (@[inApps[1]])); +} + #pragma mark Delegates Tests - (void)testDelegatesAdded { CTMultiDelegateManager *delegateManager = [[CTMultiDelegateManager alloc] init]; @@ -646,11 +749,11 @@ - (void)testOnBatchSentRemoveAll { CLTAP_INAPP_SUPPRESSED_META_KEY: @[@4, @5, @6] } ]; - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:NO]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:NO withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@1, @2, @3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@4, @5, @6]), self.evaluationManager.suppressedClientSideInApps); - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[]), self.evaluationManager.suppressedClientSideInApps); } @@ -671,18 +774,18 @@ - (void)testOnBatchSentRemoveElements { } ]; // If batch is not successful, do not remove elements - [self.evaluationManager onBatchSent:batchWithHeader withSuccess:NO]; + [self.evaluationManager onBatchSent:batchWithHeader withSuccess:NO withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@1, @2, @3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@4, @5, @6]), self.evaluationManager.suppressedClientSideInApps); // Remove only the first n elements in the batch - [self.evaluationManager onBatchSent:batchWithHeader withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeader withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[@3]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[@5, @6]), self.evaluationManager.suppressedClientSideInApps); // Remove all elements, ensure no out of range exception // Current values are @[@3] and @[@5, @6] - [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES]; + [self.evaluationManager onBatchSent:batchWithHeaderAll withSuccess:YES withQueueType:CTQueueTypeEvents]; XCTAssertEqualObjects((@[]), self.evaluationManager.evaluatedServerSideInAppIds); XCTAssertEqualObjects((@[]), self.evaluationManager.suppressedClientSideInApps); } diff --git a/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m b/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m index 56160a59..fd623647 100644 --- a/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppFCManagerTest.m @@ -33,7 +33,7 @@ @implementation CTInAppFCManagerMock @interface CTInAppFCManagerTest : XCTestCase @property (nonatomic, strong) CTInAppFCManagerMock *inAppFCManager; -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; @property (nonatomic, strong) InAppHelper *helper; @end @@ -47,7 +47,7 @@ - (void)setUp { // Set to the reset values self.inAppFCManager.globalSessionMax = 1; self.inAppFCManager.maxPerDayCount = 1; - self.prefetchManager = helper.imagePrefetchManager; + self.fileDownloader = helper.fileDownloader; } - (void)tearDown { @@ -75,7 +75,7 @@ - (void)testDidShow { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.inAppFCManager didShow:notif]; XCTAssertEqual(1, [[self.inAppFCManager.impressionManager getImpressions:@"1"] count]); XCTAssertEqual(1, [self.inAppFCManager shownTodayCount]); @@ -109,7 +109,7 @@ - (void)testRemoveStaleInAppCounts { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.inAppFCManager didShow:notif]; XCTAssertNotNil(self.inAppFCManager.inAppCounts[@"1"]); @@ -121,7 +121,7 @@ - (void)testRemoveStaleInAppCountsRemovesTriggersAndImpressions { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.helper.imagePrefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.helper.inAppTriggerManager incrementTrigger:@"1"]; [self.inAppFCManager didShow:notif]; XCTAssertNotNil(self.inAppFCManager.inAppCounts[@"1"]); @@ -193,7 +193,7 @@ - (void)testSessionCapacityMaxedOutGlobal { }; self.inAppFCManager.globalSessionMax = 5; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(-1, notif.maxPerSession); // Record 4 impressions [self recordImpressions:4]; @@ -221,7 +221,7 @@ - (void)testSessionCapacityMaxedOutGlobalLegacy { }; self.inAppFCManager.globalSessionMax = 5; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(1000, notif.maxPerSession); // Record 4 impressions [self recordImpressions:4]; @@ -241,7 +241,7 @@ - (void)testSessionCapacityMaxedOutInApp { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; self.inAppFCManager.impressionManager.sessionImpressions = [@{} mutableCopy]; // InApp session max will default to -1 on notification level XCTAssertEqual(-1, notif.maxPerSession); @@ -262,7 +262,7 @@ - (void)testSessionCapacityMaxedOutInApp { @"ti": @1, @"mdc": @1 }; - notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc1000 imagePrefetchManager:self.prefetchManager]; + notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc1000]; XCTAssertEqual(1, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{} mutableCopy]; @@ -277,7 +277,7 @@ - (void)testSessionCapacityMaxedOutInApp { @"ti": @1, @"mdc": @3 }; - notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 imagePrefetchManager:self.prefetchManager]; + notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3]; XCTAssertEqual(3, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{ @"1": @2 @@ -310,7 +310,7 @@ - (void)testSessionCapacityMaxedOutInAppLegacy { @"html": @"" } }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3 imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inAppMdc3]; XCTAssertEqual(3, notif.maxPerSession); self.inAppFCManager.impressionManager.sessionImpressions = [@{ @"1": @2 @@ -329,7 +329,7 @@ - (void)testLifetimeCapacityMaxedOut { @"ti": @1, @"tlc": @5 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(5, notif.totalLifetimeCount); XCTAssertFalse([self.inAppFCManager hasLifetimeCapacityMaxedOut:notif]); @@ -347,7 +347,7 @@ - (void)testDailyCapacityMaxedOutGlobalDefault { NSDictionary *inApp = @{ @"ti": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(-1, notif.totalDailyCount); XCTAssertFalse([self.inAppFCManager hasDailyCapacityMaxedOut:notif]); // Max Default is 1 @@ -361,7 +361,7 @@ - (void)testDailyCapacityMaxedOutGlobal { @"ti": @1, @"tdc": @10 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(10, notif.totalDailyCount); XCTAssertFalse([self.inAppFCManager hasDailyCapacityMaxedOut:notif]); @@ -380,7 +380,7 @@ - (void)testDailyCapacityMaxedOutInApp { @"ti": @1, @"tdc": @5 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; XCTAssertEqual(5, notif.totalDailyCount); // Record 4 impressions @@ -403,7 +403,7 @@ - (void)testHasInAppFrequencyLimitsMaxedOut { } ] }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; // 1 impression is in limit [self.inAppFCManager.impressionManager recordImpression:notif.Id]; @@ -421,7 +421,7 @@ - (void)testCanShowExcludeCaps { @"efc": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertTrue([self.inAppFCManager canShow:notif]); @@ -433,7 +433,7 @@ - (void)testCanShowExcludeGlobalCaps { @"excludeGlobalFCaps": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertTrue([self.inAppFCManager canShow:notif]); @@ -444,7 +444,7 @@ - (void)testCanShowMaxedOut { @"ti": @1, @"tdc": @1 }; - CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp imagePrefetchManager:self.prefetchManager]; + CTInAppNotification *notif = [[CTInAppNotification alloc] initWithJSON:inApp]; [self.inAppFCManager recordImpression:notif.Id]; XCTAssertFalse([self.inAppFCManager canShow:notif]); diff --git a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h b/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h deleted file mode 100644 index a9925444..00000000 --- a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManager+Tests.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// CTInAppImagePrefetchManager+Tests.h -// CleverTapSDKTests -// -// Created by Nikola Zagorchev on 9.01.24. -// Copyright © 2024 CleverTap. All rights reserved. -// - -@interface CTInAppImagePrefetchManager (Tests) - -@property (nonatomic, strong) NSMutableSet *activeImageSet; -@property (nonatomic, strong) NSMutableSet *inactiveImageSet; - -- (void)prefetchURLs:(NSArray *)mediaURLs; -- (NSString *)storageKeyWithSuffix:(NSString *)suffix; -- (NSArray *)getImageURLs:(NSArray *)csInAppNotifs; -- (void)removeInactiveExpiredAssets:(long)lastDeletedTime; -- (long)getLastDeletedTimestamp; - -@end diff --git a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m b/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m deleted file mode 100644 index 495268a1..00000000 --- a/CleverTapSDKTests/InApps/CTInAppImagePrefetchManagerTest.m +++ /dev/null @@ -1,295 +0,0 @@ -#import -#import -#import "CTInAppImagePrefetchManager.h" -#import "InAppHelper.h" -#import "CTPreferences.h" -#import "CTConstants.h" -#import -#import "CTInAppImagePrefetchManager+Tests.h" - -NSString * const imageURLMatch = @"ct_test_image"; - -NSString * const imageResourcePath = @"clevertap-logo"; -NSString * const imageResourceType = @"png"; - -@interface CTInAppImagePrefetchManagerTest : XCTestCase -@property (nonatomic, strong) CTInAppImagePrefetchManager *prefetchManager; -@end - -@implementation CTInAppImagePrefetchManagerTest - -- (void)setUp { - [super setUp]; - InAppHelper *helper = [InAppHelper new]; - self.prefetchManager = helper.imagePrefetchManager; - // Stub the image download request - [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - // Match requests with ct_test_image - return [request.URL.absoluteString containsString:imageURLMatch]; - } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { - // Load a local image instead of making the actual request - NSString *imagePath = [[NSBundle bundleForClass:[self class]] pathForResource:imageResourcePath ofType:imageResourceType]; - NSData *imageData = [NSData dataWithContentsOfFile:imagePath]; - - return [HTTPStubsResponse responseWithData:imageData statusCode:200 headers:nil]; - }]; -} - -- (void)tearDown { - [super tearDown]; - [HTTPStubs removeAllStubs]; - [CTPreferences removeObjectForKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for cleanup"]; - [self.prefetchManager _clearImageAssets:NO]; - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void){ - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)preloadImagesToDisk:(NSArray *)urls { - XCTestExpectation *expectation = [self expectationWithDescription:@"Preload images"]; - // Preload Images - [self.prefetchManager prefetchURLs:urls]; - - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void){ - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (NSArray *)generateImageURLs:(int)count { - NSMutableArray *arr = [NSMutableArray new]; - for (int i = 0; i < count; i++) { - [arr addObject:[[NSString alloc] initWithFormat:@"https://clevertap.com/%@%d.%@", - imageURLMatch, i, imageResourceType]]; - } - return arr; -} - -- (void)setLastDeletedPastExpiry { - // Expiration is 2 weeks - // Set the last deleted timestamp to 15 days ago - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * (2 * 7 + 1)); - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; -} - -- (void)testGetImageURLs { - NSArray *urls = [self generateImageURLs:4]; - NSArray *csInAppNotifs = @[ - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[0], - } - }, - @{ - @"media": @{ - @"content_type": @"image/gif", - @"url": urls[1], - } - }, - @{ - @"mediaLandscape": @{ - @"content_type": @"image/jpeg", - @"url": urls[2] - } - }, - @{ - @"mediaLandscape": @{ - @"content_type": @"image/gif", - @"url": urls[3] - } - } - ]; - - NSArray *imageUrls = [self.prefetchManager getImageURLs:csInAppNotifs]; - XCTAssertEqual([imageUrls count], [urls count]); -} - -- (void)testImagePresentInDiskCache { - // Check image is present in disk cache - NSArray *urls = [self generateImageURLs:1]; - [self preloadImagesToDisk:urls]; - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNotNil(image); -} - -- (void)testPreloadClientSideInAppImages { - NSArray *urls = [self generateImageURLs:2]; - NSArray *csInAppNotifs = @[ - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[0], - } - }, - @{ - @"media": @{ - @"content_type": @"image/jpeg", - @"url": urls[1] - } - } - ]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Image preload to disk cache"]; - // Preload Images - [self.prefetchManager preloadClientSideInAppImages:csInAppNotifs]; - - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - XCTAssertNotNil([self.prefetchManager loadImageFromDisk:urls[0]]); - XCTAssertNotNil([self.prefetchManager loadImageFromDisk:urls[1]]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testPreloadImages { - // 1. Test preloading images adds to active assets - NSArray *urls = [self generateImageURLs:3]; - // Load the images to disk, new images are directly added to the active set - [self preloadImagesToDisk:@[urls[0], urls[1]]]; - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - - // 2. Test preloading already present images removes them from inactive assets - // Set the inactive URLs set - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:urls]; - [self.prefetchManager setInactiveImageSet:urlsSet]; - - // Load the images to disk again, so the already saved ones are removed from the inactive set - [self preloadImagesToDisk:@[urls[0], urls[1]]]; - - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 2); - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 1); - - // 3. Test preloading images triggers remove of expired assets when expiration time has come - [self setLastDeletedPastExpiry]; - [self preloadImagesToDisk:@[urls[0]]]; - // Active assets are moved to inactive to start expiration again - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 0); - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 2); -} - -- (void)testClearAllImageAssets { - // Save the image - NSArray *urls = [self generateImageURLs:1]; - [self preloadImagesToDisk:urls]; - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNotNil(image); - - [self.prefetchManager _clearImageAssets:NO]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear all assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - UIImage *image = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNil(image); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testClearInactiveImageAssets { - // Save the image - NSArray *urls = [self generateImageURLs:2]; - [self preloadImagesToDisk:urls]; - - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:@[urls[0]]]; - [self.prefetchManager setInactiveImageSet:urlsSet]; - - [self setLastDeletedPastExpiry]; - [self.prefetchManager _clearImageAssets:YES]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear inactive assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - UIImage *inactiveImage = [self.prefetchManager loadImageFromDisk:urls[0]]; - XCTAssertNil(inactiveImage); - UIImage *activeImage = [self.prefetchManager loadImageFromDisk:urls[1]]; - XCTAssertNotNil(activeImage); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testSetImageAssetsInactiveAndClearExpired { - // Save the image - NSArray *urls = [self generateImageURLs:2]; - [self preloadImagesToDisk:urls]; - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - - [self.prefetchManager setImageAssetsInactiveAndClearExpired]; - // Last deleted date has not passed, images are moved to inactive assets only - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 2); - - [self setLastDeletedPastExpiry]; - [self.prefetchManager setImageAssetsInactiveAndClearExpired]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Clear disk cache"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - XCTAssertEqual([[self.prefetchManager activeImageSet] count], 0); - XCTAssertNil([self.prefetchManager loadImageFromDisk:urls[0]]); - XCTAssertNil([self.prefetchManager loadImageFromDisk:urls[1]]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:2.5]; -} - -- (void)testRemoveInactiveExpiredAssetsUpdatesDeletedTs { - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * (2 * 7 + 1)); - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - NSMutableSet *urlsSet = [[NSMutableSet alloc] initWithArray:[self generateImageURLs:4]]; - [self.prefetchManager setActiveImageSet:urlsSet]; - // Timestamp is expired, will trigger delete of expired assets - // There are no inactive assets to be removed - // Last deleted timestamp must be updated - // Active assets must be moved to inactive - XCTAssertEqual([[self.prefetchManager inactiveImageSet] count], 0); - [self.prefetchManager removeInactiveExpiredAssets:ts]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Remove expired assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual([urlsSet count], [[self.prefetchManager inactiveImageSet] count]); - XCTAssertEqual(0, [[self.prefetchManager activeImageSet] count]); - XCTAssertNotEqual(ts, [self.prefetchManager getLastDeletedTimestamp]); - // Must be set to current timestamp (now and set timestamp might differ slightly) - XCTAssertTrue([[NSDate date] timeIntervalSince1970] - [self.prefetchManager getLastDeletedTimestamp] < 3); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:1.5]; -} - -- (void)testRemoveInactiveExpiredAssetsNoop { - long ts = (long) [[NSDate date] timeIntervalSince1970] - (60 * 60 * 24 * 12); // 12 days - [CTPreferences putInt:ts - forKey:[self.prefetchManager storageKeyWithSuffix:CLTAP_PREFS_CS_INAPP_ASSETS_LAST_DELETED_TS]]; - NSArray *urls = [self generateImageURLs:5]; - NSMutableSet *urlsSetActive = [[NSMutableSet alloc] initWithArray:@[urls[0], urls[1]]]; - [self.prefetchManager setActiveImageSet:urlsSetActive]; - NSMutableSet *urlsSetInactive = [[NSMutableSet alloc] initWithArray:@[urls[2], urls[3], urls[4]]]; - [self.prefetchManager setInactiveImageSet:urlsSetInactive]; - // Timestamp is not expired, will not trigger delete of expired assets - // No assets must be removed - // Active and Inactive assets must remain the same - // Delete timestamp must not change - [self.prefetchManager removeInactiveExpiredAssets:ts]; - XCTestExpectation *expectation = [self expectationWithDescription:@"Remove expired assets"]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void) { - XCTAssertEqual(ts, [self.prefetchManager getLastDeletedTimestamp]); - XCTAssertEqual([urlsSetActive count], [[self.prefetchManager activeImageSet] count]); - XCTAssertEqual([urlsSetInactive count], [[self.prefetchManager inactiveImageSet] count]); - [expectation fulfill]; - }); - - [self waitForExpectations:@[expectation] timeout:1.5]; -} - -@end diff --git a/CleverTapSDKTests/InApps/CTInAppStoreTest.m b/CleverTapSDKTests/InApps/CTInAppStoreTest.m index 70d5f10f..750f7244 100644 --- a/CleverTapSDKTests/InApps/CTInAppStoreTest.m +++ b/CleverTapSDKTests/InApps/CTInAppStoreTest.m @@ -330,7 +330,7 @@ - (void)testInAppsQueue { - (void)testSwitchUserDelegateAdded { CTMultiDelegateManager *delegateManager = [[CTMultiDelegateManager alloc] init]; NSUInteger count = [[delegateManager switchUserDelegates] count]; - __unused CTInAppStore *store = [[CTInAppStore alloc] initWithConfig:self.helper.config delegateManager:delegateManager imagePrefetchManager:self.helper.imagePrefetchManager deviceId:self.helper.deviceId]; + __unused CTInAppStore *store = [[CTInAppStore alloc] initWithConfig:self.helper.config delegateManager:delegateManager deviceId:self.helper.deviceId]; XCTAssertEqual([[delegateManager switchUserDelegates] count], count + 1); } diff --git a/CleverTapSDKTests/InApps/CTNotificationActionTest.m b/CleverTapSDKTests/InApps/CTNotificationActionTest.m new file mode 100644 index 00000000..8050bfc5 --- /dev/null +++ b/CleverTapSDKTests/InApps/CTNotificationActionTest.m @@ -0,0 +1,137 @@ +// +// CTNotificationActionTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTNotificationAction.h" +#import "CTNotificationButton.h" +#import "CTConstants.h" + +@interface CTNotificationActionTest : XCTestCase + +@end + +@implementation CTNotificationActionTest + +- (void)testInitWithJSONCustom { + NSDictionary *json = @{ + @"android": @"", + @"close": @1, + @"ios": @"", + @"kv": @{}, + @"templateId": @"6633c45ae2a2f07007c031a6", + @"templateName": @"Function1", + @"type": @"custom-code", + @"vars": @{ + @"string": @"hello" + } + }; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithJSON:json]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeCustom); + XCTAssertEqualObjects(notificationAction.customTemplateInAppData.templateId, @"6633c45ae2a2f07007c031a6"); + XCTAssertEqualObjects(notificationAction.customTemplateInAppData.templateName, @"Function1"); + XCTAssertEqualObjects(notificationAction.customTemplateInAppData.args, (@{ + @"string" : @"hello" + })); +} + +- (void)testInitWithJSONOpenURL { + NSDictionary *json = @{ + @"android": @"https://example.com/", + @"close": @1, + @"ios": @"https://example.com/", + @"kv": @{}, + @"type": @"url" + }; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithJSON:json]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeOpenURL); + XCTAssertTrue([notificationAction.actionURL.absoluteString isEqualToString:@"https://example.com/"]); + XCTAssertNil(notificationAction.customTemplateInAppData); +} + +- (void)testInitWithJSONKV { + NSDictionary *json = @{ + @"android": @"", + @"close": @1, + @"ios": @"", + @"kv": @{ + @"key": @"value" + }, + @"type": @"kv" + }; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithJSON:json]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeKeyValues); + XCTAssertNil(notificationAction.customTemplateInAppData); + XCTAssertEqualObjects(notificationAction.keyValues, (@{ + @"key" : @"value" + })); +} + +- (void)testInitWithJSONClose { + NSDictionary *json = @{ + @"android": @"", + @"close": @1, + @"ios": @"", + @"kv": @{}, + @"type": @"close" + }; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithJSON:json]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeClose); +} + +- (void)testInitWithJSONRFP { + NSDictionary *json = @{ + @"android": @"", + @"close": @1, + @"ios": @"", + @"kv": @{}, + @"fbSettings": @1, + @"type": @"rfp" + }; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithJSON:json]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeRequestForPermission); + XCTAssertEqual(notificationAction.fallbackToSettings, YES); +} + +- (void)testInitWithNotificationButton { + NSDictionary *json = @{ + @"actions": @{ + @"android": @"", + @"close": @1, + @"ios": @"https://example.com/", + @"kv": @{ + @"key": @"value" + }, + @"type": @"url", + @"fbSettings": @1 + } + }; + CTNotificationButton *notificationButton = [[CTNotificationButton alloc] initWithJSON:json]; + + XCTAssertNotNil(notificationButton.action); + XCTAssertEqual(notificationButton.type, CTInAppActionTypeOpenURL); + XCTAssertTrue([notificationButton.actionURL.absoluteString isEqualToString:@"https://example.com/"]); + XCTAssertEqual(notificationButton.fallbackToSettings, YES); + XCTAssertEqualObjects(notificationButton.customExtras, (@{ + @"key" : @"value" + })); +} + +- (void)testInitWithOpenURL { + NSURL *url = [[NSURL alloc] initWithString:@"https://example.com/"]; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithOpenURL:url]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeOpenURL); + XCTAssertEqualObjects(notificationAction.actionURL, url); +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateBuilderTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateBuilderTest.m new file mode 100644 index 00000000..cf28f803 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateBuilderTest.m @@ -0,0 +1,224 @@ +// +// CTCustomTemplateBuilderTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 7.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CTCustomTemplatesManager.h" +#import "CTInAppTemplateBuilder.h" +#import "CTAppFunctionBuilder.h" +#import "CTCustomTemplate-Internal.h" +#import "CTTemplatePresenterMock.h" + +@interface CTCustomTemplateBuilderTest : XCTestCase + +@end + +@implementation CTCustomTemplateBuilderTest + +- (void)testNameNotSetThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + XCTAssertThrows([templateBuilder build]); + + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + XCTAssertThrows([functionBuilder build]); +} + +- (void)testEmptyNameSetThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + XCTAssertThrows([templateBuilder setName:@""]); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows([templateBuilder setName:nil]); +#pragma clang diagnostic pop +} + +- (void)testNameAlreadySetThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"template"]; + XCTAssertThrows([templateBuilder setName:@"template"]); +} + +- (void)testInvalidArgumentNameThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + XCTAssertThrows([templateBuilder addArgument:@"" withString:@"string"]); + XCTAssertThrows([templateBuilder addArgument:@".start" withString:@"string"]); + XCTAssertThrows([templateBuilder addArgument:@"end." withString:@"string"]); + XCTAssertThrows([templateBuilder addArgument:@"two.." withString:@"string"]); +} + +- (void)testValidArgumentName { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + XCTAssertNoThrow([templateBuilder addArgument:@"name" withString:@"string"]); + XCTAssertNoThrow([templateBuilder addArgument:@"arg name" withString:@"string"]); + XCTAssertNoThrow([templateBuilder addArgument:@"valid.name" withString:@"string"]); + XCTAssertNoThrow([templateBuilder addArgument:@"valid.two.name" withString:@"string"]); +} + +- (void)testInvalidArgumentValueDictionaryThrows { + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincompatible-pointer-types" + XCTAssertThrows([functionBuilder addArgument:@"invalid type" withString:[NSDictionary dictionary]]); +#pragma clang diagnostic pop +} + +- (void)testArgumentEmptyDictionaryThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + XCTAssertThrows([templateBuilder addArgument:@"dictionary" withDictionary:@{}]); + XCTAssertNoThrow([templateBuilder addArgument:@"dictionary" withDictionary:@{ @"a": @(0) }]); +} + +- (void)testArgumentSameNameThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"arg" withString:@"value"]; + XCTAssertThrows([templateBuilder addArgument:@"arg" withString:@"value"]); + XCTAssertThrows([templateBuilder addArgument:@"arg" withNumber:@(2)]); + XCTAssertThrows([templateBuilder addArgument:@"arg" withBool:YES]); + XCTAssertThrows([templateBuilder addFileArgument:@"arg"]); + XCTAssertThrows([templateBuilder addActionArgument:@"arg"]); + XCTAssertThrows([templateBuilder addArgument:@"arg" withDictionary:@{ @"a": @(0) }]); +} + +- (void)testArgumentDictionaryName { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"arg" withDictionary:@{ @"a": @(0) }]; + [templateBuilder addArgument:@"arg" withDictionary:@{ @"b": @(0) }]; + XCTAssertThrows([templateBuilder addArgument:@"arg" withDictionary:@{ @"a": @(0) }]); +} + +- (void)testNoPresenterThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"template"]; + XCTAssertThrows([templateBuilder build]); +} + +- (void)testParentArgsAlreadyDefinedThrows { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"a.b" withString:@""]; + XCTAssertThrows([templateBuilder addArgument:@"a" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a.b" withString:@""]); + + templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"a.b.c.d" withString:@""]; + XCTAssertThrows([templateBuilder addArgument:@"a" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a.b" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a.b.c" withString:@""]); + [templateBuilder addArgument:@"a.b.c.e" withString:@""]; + + + templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"a.a.a" withString:@""]; + [templateBuilder addArgument:@"a.a.b" withString:@""]; + XCTAssertThrows([templateBuilder addArgument:@"a.a.a.d" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a.a" withString:@""]); + + templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"a.a.a" withString:@""]; + [templateBuilder addArgument:@"a.a.b" withString:@""]; + XCTAssertThrows([templateBuilder addArgument:@"a.a.a.d" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a" withString:@""]); + XCTAssertThrows([templateBuilder addArgument:@"a.a" withString:@""]); + [templateBuilder addArgument:@"a.a.c" withString:@""]; +} + +- (void)testArgumentValue { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder addArgument:@"a" withString:@""]; + [templateBuilder addArgument:@"b" withBool:nil]; + [templateBuilder addFileArgument:@"c"]; + [templateBuilder addActionArgument:@"d"]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincompatible-pointer-types" +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows([templateBuilder addArgument:@"e" withString:nil]); + XCTAssertThrows([templateBuilder addArgument:@"f" withString:[NSNull null]]); + XCTAssertThrows([templateBuilder addArgument:@"g" withNumber:nil]); + XCTAssertThrows([templateBuilder addArgument:@"h" withDictionary:nil]); +#pragma clang diagnostic pop +} + +- (void)testArgumentOrder { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"template"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templateBuilder addArgument:@"a" withString:@""]; + [templateBuilder addArgument:@"b" withBool:YES]; + [templateBuilder addFileArgument:@"c"]; + [templateBuilder addActionArgument:@"d"]; + + CTCustomTemplate *template = [templateBuilder build]; + XCTAssertEqualObjects(template.arguments[0].name, @"a"); + XCTAssertEqualObjects(template.arguments[1].name, @"b"); + XCTAssertEqualObjects(template.arguments[2].name, @"c"); + XCTAssertEqualObjects(template.arguments[3].name, @"d"); +} + +- (void)testFlatDictionaryArgument { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"template"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templateBuilder addArgument:@"e" withDictionary:@{ + @"g": @"value", + @"h": @1, + @"f": @{ + @"c": @10, + @"e": @"", + @"d": @99 + } + }]; + + CTCustomTemplate *template = [templateBuilder build]; + NSSet *expected = [[NSSet alloc] initWithArray: @[ + [[CTTemplateArgument alloc] initWithName:@"e.h" type:CTTemplateArgumentTypeNumber defaultValue:@1], + [[CTTemplateArgument alloc] initWithName:@"e.g" type:CTTemplateArgumentTypeString defaultValue:@"value"], + [[CTTemplateArgument alloc] initWithName:@"e.f.c" type:CTTemplateArgumentTypeNumber defaultValue:@10], + [[CTTemplateArgument alloc] initWithName:@"e.f.e" type:CTTemplateArgumentTypeString defaultValue:@""], + [[CTTemplateArgument alloc] initWithName:@"e.f.d" type:CTTemplateArgumentTypeNumber defaultValue:@99] + ]]; + XCTAssertEqualObjects([[NSSet alloc] initWithArray:template.arguments], expected); +} + +- (void)testArguments { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"template"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templateBuilder addArgument:@"string" withString:@"string value"]; + [templateBuilder addArgument:@"string 2" withString:@"string value 2"]; + [templateBuilder addArgument:@"bool" withBool:YES]; + [templateBuilder addArgument:@"number" withNumber:@(9999.99999999999)]; + [templateBuilder addArgument:@"int" withNumber:[NSNumber numberWithInteger:12345]]; + [templateBuilder addArgument:@"float" withNumber:[NSNumber numberWithFloat:1.99]]; + [templateBuilder addFileArgument:@"file"]; + [templateBuilder addActionArgument:@"action"]; + + CTCustomTemplate *template = [templateBuilder build]; + NSSet *expected = [[NSSet alloc] initWithArray: @[ + [[CTTemplateArgument alloc] initWithName:@"string" type:CTTemplateArgumentTypeString defaultValue:@"string value"], + [[CTTemplateArgument alloc] initWithName:@"string 2" type:CTTemplateArgumentTypeString defaultValue:@"string value 2"], + [[CTTemplateArgument alloc] initWithName:@"bool" type:CTTemplateArgumentTypeBool defaultValue:@(YES)], + [[CTTemplateArgument alloc] initWithName:@"number" type:CTTemplateArgumentTypeNumber defaultValue:@(9999.99999999999)], + [[CTTemplateArgument alloc] initWithName:@"int" type:CTTemplateArgumentTypeNumber defaultValue:@(12345)], + [[CTTemplateArgument alloc] initWithName:@"float" type:CTTemplateArgumentTypeNumber defaultValue:@(1.99f)], + [[CTTemplateArgument alloc] initWithName:@"file" type:CTTemplateArgumentTypeFile defaultValue:nil], + [[CTTemplateArgument alloc] initWithName:@"action" type:CTTemplateArgumentTypeAction defaultValue:nil], + ]]; + XCTAssertEqualObjects([[NSSet alloc] initWithArray:template.arguments], expected); +} + +- (void)testFunctionArgumentDictionary { + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [functionBuilder addArgument:@"arg" withDictionary:@{ @"a": @"value" }]; + [functionBuilder setName:@"function"]; + [functionBuilder setPresenter:[CTTemplatePresenterMock new]]; + CTCustomTemplate *template = [functionBuilder build]; + CTTemplateArgument *arg = [[CTTemplateArgument alloc] initWithName:@"arg.a" type:CTTemplateArgumentTypeString defaultValue:@"value"]; + XCTAssertEqualObjects(arg, template.arguments.firstObject); +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m new file mode 100644 index 00000000..54a4d483 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m @@ -0,0 +1,127 @@ +// +// CTCustomTemplateInAppDataTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTCustomTemplateInAppData.h" +#import "CTCustomTemplateInAppData-Internal.h" +#import "CTInAppNotification.h" +#import "CTConstants.h" + +@interface CTCustomTemplateInAppDataTest : XCTestCase + +@end + +@implementation CTCustomTemplateInAppDataTest + +- (NSDictionary *)jsonCustomCode { + return @{ + CLTAP_INAPP_TEMPLATE_ID: @"templateId", + CLTAP_INAPP_TEMPLATE_NAME: @"templateName", + CLTAP_INAPP_TYPE: @"custom-code", + CLTAP_INAPP_TEMPLATE_DESCRIPTION: @"templateDescription", + CLTAP_INAPP_VARS: @{ + @"key1": @"value1", + @"key2": @"value2" + } + }; +} + +- (void)testCreateWithJSON { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; + + XCTAssertEqualObjects(customTemplate.templateName, @"templateName"); + XCTAssertEqualObjects(customTemplate.templateId, @"templateId"); + XCTAssertEqualObjects(customTemplate.templateDescription, @"templateDescription"); + XCTAssertEqualObjects(customTemplate.args, (@{ + @"key1": @"value1", + @"key2": @"value2" + })); +} + +- (void)testCreateWithJSONNotCustomCode { + NSDictionary *json = @{ + CLTAP_INAPP_TEMPLATE_ID: @"templateId", + CLTAP_INAPP_TEMPLATE_NAME: @"templateName", + CLTAP_INAPP_TYPE: @"interstitial", + CLTAP_INAPP_TEMPLATE_DESCRIPTION: @"templateDescription", + CLTAP_INAPP_VARS: @{ + @"key1": @"value1", + @"key2": @"value2" + } + }; + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:json]; + XCTAssertNil(customTemplate); +} + +- (void)testCreateWithJSONNoType { + NSDictionary *json = @{ + CLTAP_INAPP_TEMPLATE_ID: @"templateId", + CLTAP_INAPP_TEMPLATE_NAME: @"templateName", + CLTAP_INAPP_TEMPLATE_DESCRIPTION: @"templateDescription", + CLTAP_INAPP_VARS: @{ + @"key1": @"value1", + @"key2": @"value2" + } + }; + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:json]; + XCTAssertNil(customTemplate); +} + +- (void)testCreateFromInAppNotification { + NSDictionary *json = @{ + @"templateDescription": @"", + @"templateId": @"6633c400e2a2f07007c031a5", + @"templateName": @"templateName", + @"ti": @1715349815, + @"type": @"custom-code", + @"vars": @{ + @"number": @123, + @"string": @"hello", + }, + @"wzrk_id": @"1715349815_20240510" + }; + + CTInAppNotification *inApp = [[CTInAppNotification alloc] initWithJSON:json]; + CTCustomTemplateInAppData *customTemplateData = inApp.customTemplateInAppData; + XCTAssertEqualObjects(customTemplateData.templateName, @"templateName"); + XCTAssertEqualObjects(customTemplateData.templateId, @"6633c400e2a2f07007c031a5"); + XCTAssertEqualObjects(customTemplateData.templateDescription, @""); + XCTAssertEqualObjects(customTemplateData.args, (@{ + @"number": @123, + @"string": @"hello", + })); +} + +- (void)testSetIsAction { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; + [customTemplate setIsAction:YES]; + + XCTAssertEqual(customTemplate.json[@"is_action"], @(YES)); +} + +- (void)testCopy { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; + + CTCustomTemplateInAppData *copy = [customTemplate copy]; + // Verify not the same instance + XCTAssertNotEqual(customTemplate, copy); + + // Verify property values match + XCTAssertEqualObjects(customTemplate.templateId, copy.templateId); + XCTAssertEqualObjects(customTemplate.templateName, copy.templateName); + XCTAssertEqualObjects(customTemplate.templateDescription, copy.templateDescription); + XCTAssertEqualObjects(customTemplate.args, copy.args); + XCTAssertEqualObjects(customTemplate.json, copy.json); + XCTAssertEqual(customTemplate.isAction, copy.isAction); + + // Verify copied properties are not the same instance (strings are immutable) + XCTAssertNotEqual(customTemplate.args, copy.args); + XCTAssertNotEqual(customTemplate.json, copy.json); +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m new file mode 100644 index 00000000..e546264b --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m @@ -0,0 +1,42 @@ +// +// CTCustomTemplateTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 7.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CTCustomTemplate-Internal.h" +#import "CTCustomTemplateBuilder.h" + +@interface CTCustomTemplateTest : XCTestCase + +@end + +@implementation CTCustomTemplateTest + +- (void)testEqual { + CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:FUNCTION_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + XCTAssertEqualObjects(template, template); + XCTAssertEqualObjects(template, sameTemplate); + XCTAssertEqualObjects(template, sameName); + XCTAssertNotEqualObjects(template, differentName); + XCTAssertNotEqualObjects(template, @"template"); +} + +- (void)testHash { + CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:FUNCTION_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + XCTAssertEqual([template hash], [sameTemplate hash]); + XCTAssertEqual([template hash], [sameName hash]); + XCTAssertNotEqual([template hash], [differentName hash]); +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManager+Tests.h b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManager+Tests.h new file mode 100644 index 00000000..deddada4 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManager+Tests.h @@ -0,0 +1,19 @@ +// +// CTCustomTemplatesManager+Tests.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 11.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTCustomTemplatesManager_Tests_h +#define CTCustomTemplatesManager_Tests_h +#import "CTCustomTemplatesManager.h" + +@interface CTCustomTemplatesManager (Tests) + ++ (void)clearTemplateProducers; + +@end + +#endif /* CTCustomTemplatesManager_Tests_h */ diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m new file mode 100644 index 00000000..7d72a627 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m @@ -0,0 +1,541 @@ +// +// CTCustomTemplatesManagerTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 28.02.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CTCustomTemplatesManager-Internal.h" +#import "CTCustomTemplatesManager+Tests.h" +#import "CTInAppTemplateBuilder.h" +#import "CTAppFunctionBuilder.h" +#import "CTTemplatePresenterMock.h" +#import "CTTestTemplateProducer.h" +#import "CTInAppNotificationDisplayDelegateMock.h" +#import "CTFileDownloaderCustomTemplatesMock.h" + +@interface CTCustomTemplatesManagerTest : XCTestCase + +@property CTFileDownloader* fileDownloader; + +@end + +@implementation CTCustomTemplatesManagerTest + +- (void)setUp { + self.fileDownloader = [[CTFileDownloaderCustomTemplatesMock alloc] initWithConfig:self.instanceConfig]; +} + +- (void)tearDown { + [super tearDown]; + [CTCustomTemplatesManager clearTemplateProducers]; +} + +- (void)testSyncPayloadComplex { + NSMutableSet *templates = [NSMutableSet set]; + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"Template 1"]; + [templateBuilder addArgument:@"b" withBool:NO]; + [templateBuilder addArgument:@"c" withString:@"1 string"]; + [templateBuilder addArgument:@"d" withString:@"2 string"]; + [templateBuilder addArgument:@"e" withDictionary:@{ + @"h": @7, + @"f": @{ + @"c": @4, + @"e": @"6 string", + @"d": @5 + } + }]; + [templateBuilder addArgument:@"l" withString:@"9 string"]; + [templateBuilder addArgument:@"k" withNumber:@10]; + [templateBuilder addArgument:@"e.w" withNumber:@8]; + [templateBuilder addArgument:@"e.f.a" withNumber:@3]; + [templateBuilder addArgument:@"a" withDictionary: @{ + @"n": @"12 string", + @"m": @"11 string" + }]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder build]]; + + CTInAppTemplateBuilder *templateBuilderTemplate2 = [CTInAppTemplateBuilder new]; + [templateBuilderTemplate2 setName:@"Template 2"]; + [templateBuilderTemplate2 addArgument:@"b" withBool:NO]; + [templateBuilderTemplate2 addArgument:@"c" withString:@"1 string"]; + [templateBuilderTemplate2 addArgument:@"a.d" withNumber:@5]; + [templateBuilderTemplate2 addArgument:@"a.c.a" withNumber:@4]; + [templateBuilderTemplate2 addArgument:@"a" withDictionary: @{ + @"b": @"3 string", + @"a": @"2 string" + }]; + [templateBuilderTemplate2 setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilderTemplate2 build]]; + + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [functionBuilder setName:@"Function 1"]; + [functionBuilder addArgument:@"b" withBool:NO]; + [functionBuilder addArgument:@"a" withString:@"1 string"]; + [functionBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[functionBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + [CTCustomTemplatesManager registerTemplateProducer:producer]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + NSDictionary *syncPayload = [manager syncPayload]; + + NSDictionary *expectedPayload = @{ + @"definitions": @{ + @"Function 1": @{ + @"type": FUNCTION_TYPE, + @"vars": @{ + @"b": @{ + @"defaultValue": @0, + @"order": @0, + @"type": @"boolean" + }, + @"a": @{ + @"defaultValue": @"1 string", + @"order": @1, + @"type": @"string" + } + } + }, + @"Template 1": @{ + @"type": TEMPLATE_TYPE, + @"vars": @{ + @"a.m": @{ + @"defaultValue": @"11 string", + @"order": @11, + @"type": @"string" + }, + @"a.n": @{ + @"defaultValue": @"12 string", + @"order": @12, + @"type": @"string" + }, + @"b": @{ + @"defaultValue": @0, + @"order": @0, + @"type": @"boolean" + }, + @"c": @{ + @"defaultValue": @"1 string", + @"order": @1, + @"type": @"string" + }, + @"d": @{ + @"defaultValue": @"2 string", + @"order": @2, + @"type": @"string" + }, + @"e.f.a": @{ + @"defaultValue": @3, + @"order": @3, + @"type": @"number" + }, + @"e.f.c": @{ + @"defaultValue": @4, + @"order": @4, + @"type": @"number" + }, + @"e.f.d": @{ + @"defaultValue": @5, + @"order": @5, + @"type": @"number" + }, + @"e.f.e": @{ + @"defaultValue": @"6 string", + @"order": @6, + @"type": @"string" + }, + @"e.h": @{ + @"defaultValue": @7, + @"order": @7, + @"type": @"number" + }, + @"e.w": @{ + @"defaultValue": @8, + @"order": @8, + @"type": @"number" + }, + @"k": @{ + @"defaultValue": @10, + @"order": @10, + @"type": @"number" + }, + @"l": @{ + @"defaultValue": @"9 string", + @"order": @9, + @"type": @"string" + } + } + }, + @"Template 2": @{ + @"type": TEMPLATE_TYPE, + @"vars": @{ + @"b": @{ + @"defaultValue": @0, + @"order": @0, + @"type": @"boolean" + }, + @"c": @{ + @"defaultValue": @"1 string", + @"order": @1, + @"type": @"string" + }, + @"a.a": @{ + @"defaultValue": @"2 string", + @"order": @2, + @"type": @"string" + }, + @"a.b": @{ + @"defaultValue": @"3 string", + @"order": @3, + @"type": @"string" + }, + @"a.c.a": @{ + @"defaultValue": @4, + @"order": @4, + @"type": @"number" + }, + @"a.d": @{ + @"defaultValue": @5, + @"order": @5, + @"type": @"number" + } + } + } + }, + @"type": @"templatePayload" + }; + + XCTAssertEqual([syncPayload[@"definitions"] count], 3); + XCTAssertEqualObjects(syncPayload, expectedPayload); +} + +- (void)testSyncPayload { + NSMutableSet *templates = [NSMutableSet set]; + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:@"Template 1"]; + [templateBuilder addArgument:@"boolean" withBool:NO]; + [templateBuilder addArgument:@"string" withString:@"string"]; + [templateBuilder addArgument:@"number" withNumber:@2]; + [templateBuilder addArgument:@"dictionary" withDictionary:@{ + @"key": @"value" + }]; + [templateBuilder addFileArgument:@"file"]; + [templateBuilder addActionArgument:@"action"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + [CTCustomTemplatesManager registerTemplateProducer:producer]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + NSDictionary *syncPayload = [manager syncPayload]; + NSDictionary *expectedPayload = @{ + @"type": @"templatePayload", + @"definitions": @{ + @"Template 1": @{ + @"type": TEMPLATE_TYPE, + @"vars": @{ + @"boolean": @{ + @"defaultValue": @0, + @"order": @0, + @"type": @"boolean" + }, + @"string": @{ + @"defaultValue": @"string", + @"order": @1, + @"type": @"string" + }, + @"number": @{ + @"defaultValue": @2, + @"order": @2, + @"type": @"number" + }, + @"dictionary.key": @{ + @"defaultValue": @"value", + @"order": @3, + @"type": @"string" + }, + @"file": @{ + @"order": @4, + @"type": @"file" + }, + @"action": @{ + @"order": @5, + @"type": @"action" + } + } + } + } + }; + + XCTAssertEqual([syncPayload[@"definitions"] count], 1); + XCTAssertEqualObjects(syncPayload, expectedPayload); +} + +- (void)testTemplatesRegistered { + NSMutableSet *templates = [NSMutableSet set]; + + NSString *templateName1 = @"Template1"; + NSString *templateName2 = @"Template2"; + NSString *functionName1 = @"Function1"; + NSString *functionName2 = @"Function2"; + + CTInAppTemplateBuilder *templateBuilder1 = [CTInAppTemplateBuilder new]; + [templateBuilder1 setName:templateName1]; + [templateBuilder1 setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder1 build]]; + + CTInAppTemplateBuilder *templateBuilder2 = [CTInAppTemplateBuilder new]; + [templateBuilder2 setName:templateName2]; + [templateBuilder2 setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder2 build]]; + + CTAppFunctionBuilder *functionBuilder1 = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [functionBuilder1 setName:functionName1]; + [functionBuilder1 setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[functionBuilder1 build]]; + + CTAppFunctionBuilder *functionBuilder2 = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [functionBuilder2 setName:functionName2]; + [functionBuilder2 setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[functionBuilder2 build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + [CTCustomTemplatesManager registerTemplateProducer:producer]; + + CTCustomTemplatesManager *manager = [self templatesManager]; + + XCTAssertTrue([manager isRegisteredTemplateWithName:templateName1]); + XCTAssertTrue([manager isRegisteredTemplateWithName:templateName2]); + XCTAssertTrue([manager isRegisteredTemplateWithName:functionName1]); + XCTAssertTrue([manager isRegisteredTemplateWithName:functionName2]); + + XCTAssertFalse([manager isRegisteredTemplateWithName:@"non-existent"]); + + CleverTapInstanceConfig *config2 = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId2" accountToken:@"testAccountToken2"]; + CTCustomTemplatesManager *managerWithNewConfig = [[CTCustomTemplatesManager alloc] initWithConfig:config2]; + + XCTAssertTrue([managerWithNewConfig isRegisteredTemplateWithName:templateName1]); + XCTAssertTrue([managerWithNewConfig isRegisteredTemplateWithName:templateName2]); + XCTAssertTrue([managerWithNewConfig isRegisteredTemplateWithName:functionName1]); + XCTAssertTrue([managerWithNewConfig isRegisteredTemplateWithName:functionName2]); + + XCTAssertFalse([managerWithNewConfig isRegisteredTemplateWithName:@"non-existent"]); +} + +- (void)testDuplicateTemplateNameThrows { + NSMutableSet *templates = [NSMutableSet set]; + CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; + [templateBuilder setName:TEMPLATE_NAME]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder build]]; + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + + [CTCustomTemplatesManager registerTemplateProducer:producer]; + [CTCustomTemplatesManager registerTemplateProducer:producer]; + + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + XCTAssertThrows([[CTCustomTemplatesManager alloc] initWithConfig:config]); +} + +- (void)testPresenterOnPresent { + NSMutableSet *templates = [NSMutableSet set]; + CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; + CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; + [templateBuilder setName:TEMPLATE_NAME]; + [templateBuilder setPresenter:templatePresenter]; + [templates addObject:[templateBuilder build]]; + + CTTemplatePresenterMock *functionPresenter = [CTTemplatePresenterMock new]; + CTInAppTemplateBuilder *functionBuilder = [CTInAppTemplateBuilder new]; + [functionBuilder setName:FUNCTION_NAME]; + [functionBuilder setPresenter:functionPresenter]; + [templates addObject:[functionBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + + [CTCustomTemplatesManager registerTemplateProducer:producer]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate andFileDownloader:self.fileDownloader]; + XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); + XCTAssertEqual(TEMPLATE_NAME, templatePresenter.onPresentContext.templateName); + + CTInAppNotification *functionNotificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; + [manager presentNotification:functionNotificaton withDelegate:delegate andFileDownloader:self.fileDownloader]; + XCTAssertEqual(1, functionPresenter.onPresentInvocationsCount); + XCTAssertEqual(FUNCTION_NAME, functionPresenter.onPresentContext.templateName); +} + +- (void)testPresenterOnPresentNonRegisteredTemplate { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + // Use the simpleFunctionNotificationJson which is not registered + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate andFileDownloader:self.fileDownloader]; + XCTAssertEqual(0, templatePresenter.onPresentInvocationsCount); +} + +- (void)testActiveContextForTemplate { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate andFileDownloader:self.fileDownloader]; + XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); + CTTemplateContext *context = [manager activeContextForTemplate:TEMPLATE_NAME]; + XCTAssertEqual(templatePresenter.onPresentContext, context); + + [context dismissed]; + XCTAssertNil([manager activeContextForTemplate:TEMPLATE_NAME]); +} + +- (void)testActiveContextForInactiveTemplate { + [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + XCTAssertNil([manager activeContextForTemplate:TEMPLATE_NAME]); +} + +- (void)testOnClose { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate andFileDownloader:self.fileDownloader]; + XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); + CTTemplateContext *context = [manager activeContextForTemplate:TEMPLATE_NAME]; + XCTAssertEqual(templatePresenter.onPresentContext, context); + + [manager closeNotification:notificaton]; + XCTAssertEqual(1, templatePresenter.onCloseInvocationsCount); + XCTAssertEqual(templatePresenter.onCloseContext, context); +} + +- (void)testOnCloseNotActiveContext { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + // Not active context + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + [manager closeNotification:notificaton]; + XCTAssertEqual(0, templatePresenter.onCloseInvocationsCount); + + // Not registered template + CTInAppNotification *notificatonNotRegistered = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; + [manager closeNotification:notificatonNotRegistered]; + XCTAssertEqual(0, templatePresenter.onCloseInvocationsCount); +} + +- (void)testFileArgURLs { + NSMutableSet *templates = [NSMutableSet set]; + CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; + [templateBuilder setName:TEMPLATE_NAME]; + [templateBuilder addFileArgument:@"file"]; + [templateBuilder addFileArgument:@"file1"]; + [templateBuilder addActionArgument:@"action"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[templateBuilder build]]; + + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [functionBuilder setName:FUNCTION_NAME]; + [functionBuilder addFileArgument:@"file"]; + [functionBuilder setPresenter:[CTTemplatePresenterMock new]]; + [templates addObject:[functionBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + [CTCustomTemplatesManager registerTemplateProducer:producer]; + + NSString *url = @"url"; + NSString *url1 = @"url1"; + NSString *url2 = @"url2"; + + NSDictionary *inAppDataJson = @{ + @"templateDescription": @"", + @"templateId": @"id", + @"templateName": TEMPLATE_NAME, + @"type": @"custom-code", + @"vars": @{ + @"file": url, + @"file1": url1, + @"action": @{ + @"templateDescription": @"", + @"templateId": @"id", + @"templateName": FUNCTION_NAME, + @"type": @"custom-code", + @"vars": @{ + @"file": url2, + } + } + } + }; + CTCustomTemplateInAppData *inAppData = [CTCustomTemplateInAppData createWithJSON:inAppDataJson]; + CTCustomTemplatesManager *manager = [self templatesManager]; + NSSet *actual = [manager fileArgsURLsForInAppData:inAppData]; + NSSet *expected = [NSSet setWithObjects:url, url1, url2, nil]; + XCTAssertEqualObjects(expected, actual); + + NSSet *actualFromJSON = [manager fileArgsURLs:inAppDataJson]; + XCTAssertEqualObjects(expected, actualFromJSON); +} + +- (CTTemplatePresenterMock *)registerTemplate { + NSMutableSet *templates = [NSMutableSet set]; + CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; + CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; + [templateBuilder setName:TEMPLATE_NAME]; + [templateBuilder setPresenter:templatePresenter]; + [templates addObject:[templateBuilder build]]; + + CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; + + [CTCustomTemplatesManager registerTemplateProducer:producer]; + return templatePresenter; +} + +- (CleverTapInstanceConfig *)instanceConfig { + return [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; +} + +- (CTCustomTemplatesManager *)templatesManager { + CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:self.instanceConfig]; + return manager; +} + +- (NSDictionary *)simpleTemplateNotificationJson { + return @{ + @"templateName": TEMPLATE_NAME, + @"type": @"custom-code", + @"vars": @{} + }; +} + +- (NSDictionary *)simpleFunctionNotificationJson { + return @{ + @"templateName": FUNCTION_NAME, + @"type": @"custom-code", + @"vars": @{} + }; +} + +static NSString * const TEMPLATE_NAME = @"Template 1"; +static NSString * const FUNCTION_NAME = @"Function 1"; + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.h b/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.h new file mode 100644 index 00000000..d502af8d --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.h @@ -0,0 +1,18 @@ +// +// CTFileDownloaderCustomTemplatesMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 4.07.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTFileDownloader.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTFileDownloaderCustomTemplatesMock : CTFileDownloader + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.m b/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.m new file mode 100644 index 00000000..fc46d09f --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTFileDownloaderCustomTemplatesMock.m @@ -0,0 +1,32 @@ +// +// CTFileDownloaderCustomTemplatesMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 4.07.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTFileDownloaderCustomTemplatesMock.h" + +@implementation CTFileDownloaderCustomTemplatesMock + +- (void)downloadFiles:(NSArray *)fileURLs withCompletionBlock:(void (^ _Nullable)(NSDictionary *status))completion { + completion(@{}); +} + +- (BOOL)isFileAlreadyPresent:(NSString *)url { + return NO; +} + +- (void)clearFileAssets:(BOOL)expiredOnly { +} + +- (nullable NSString *)fileDownloadPath:(NSString *)url { + return url; +} + +- (nullable UIImage *)loadImageFromDisk:(NSString *)imageURL { + return nil; +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h new file mode 100644 index 00000000..888df7cd --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h @@ -0,0 +1,23 @@ +// +// CTInAppNotificationDisplayDelegateMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 5.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTInAppNotificationDisplayDelegate.h" +#import "CTInAppNotification.h" +#import "CTNotificationAction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTInAppNotificationDisplayDelegateMock : NSObject + +@property (nonatomic) void (^handleNotificationAction)(CTNotificationAction *, CTInAppNotification *, NSDictionary *); +@property (nonatomic) int handleNotificationActionInvocations; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m new file mode 100644 index 00000000..e32d3316 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m @@ -0,0 +1,32 @@ +// +// CTInAppNotificationDisplayDelegateMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 5.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTInAppNotificationDisplayDelegateMock.h" + +@implementation CTInAppNotificationDisplayDelegateMock + +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras { + self.handleNotificationActionInvocations++; + if (self.handleNotificationAction) { + self.handleNotificationAction(action, notification, extras); + } +} + +- (void)notificationDidDismiss:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller { +} + +- (void)notificationDidShow:(CTInAppNotification *)notification { +} + +- (void)handleInAppPushPrimer:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller withFallbackToSettings:(BOOL)isFallbackToSettings { +} + +- (void)inAppPushPrimerDidDismissed { +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateArgumentTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateArgumentTest.m new file mode 100644 index 00000000..7caea17d --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateArgumentTest.m @@ -0,0 +1,57 @@ +// +// CTTemplateArgumentTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTTemplateArgument.h" + +@interface CTTemplateArgumentTest : XCTestCase + +@end + +@implementation CTTemplateArgumentTest + +- (void)testInit { + NSString *name = @"testName"; + id defaultValue = @(42); + CTTemplateArgument *argument = [[CTTemplateArgument alloc] initWithName:name type:CTTemplateArgumentTypeNumber defaultValue:defaultValue]; + + XCTAssertEqualObjects(argument.name, name); + XCTAssertEqual(argument.type, CTTemplateArgumentTypeNumber); + XCTAssertEqualObjects(argument.defaultValue, defaultValue); +} + +- (void)testEquals { + CTTemplateArgument *argument1 = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + CTTemplateArgument *argument2 = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + CTTemplateArgument *argument3 = [[CTTemplateArgument alloc] initWithName:@"otherName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + CTTemplateArgument *argument4 = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeString defaultValue:@"test"]; + + XCTAssertEqualObjects(argument1, argument2); + XCTAssertNotEqualObjects(argument1, argument3); + XCTAssertNotEqualObjects(argument1, argument4); +} + +- (void)testHash { + CTTemplateArgument *argument1 = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + CTTemplateArgument *argument2 = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + CTTemplateArgument *argument3 = [[CTTemplateArgument alloc] initWithName:@"otherName" type:CTTemplateArgumentTypeString defaultValue:@"test"]; + + XCTAssertEqual(argument1.hash, argument2.hash); + XCTAssertNotEqual(argument1.hash, argument3.hash); +} + +- (void)testDescription { + CTTemplateArgument *argument = [[CTTemplateArgument alloc] initWithName:@"testName" type:CTTemplateArgumentTypeNumber defaultValue:@(42)]; + NSString *description = [argument description]; + + XCTAssertTrue([description containsString:@"testName"]); + XCTAssertTrue([description containsString:@"number"]); + XCTAssertTrue([description containsString:@"42"]); +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m new file mode 100644 index 00000000..4ce4484f --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m @@ -0,0 +1,486 @@ +// +// CTTemplateContextTest.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 13.05.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CTTemplateContext-Internal.h" +#import "CTInAppTemplateBuilder.h" +#import "CTAppFunctionBuilder.h" +#import "CTTemplatePresenterMock.h" +#import "CTTemplateContext-Internal.h" +#import "CTCustomTemplateInAppData-Internal.h" +#import "CTInAppNotificationDisplayDelegateMock.h" +#import "CTConstants.h" +#import "CTFileDownloaderCustomTemplatesMock.h" +#import "CleverTapInstanceConfig.h" + +@interface CTTemplateContext (Tests) + +@property (nonatomic) id notificationDelegate; + +@end + +@interface CTTemplateContextTest : XCTestCase + +@property CTFileDownloader* fileDownloader; + +@end + +@implementation CTTemplateContextTest + +- (void)setUp { + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; + self.fileDownloader = [[CTFileDownloaderCustomTemplatesMock alloc] initWithConfig:config]; +} + +- (void)testDismissedShouldCallDelegateNotificationDidDismiss { + CTTemplateContext *context = self.templateContext; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context dismissed]; + [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; + + // should call delegate dismiss only once + [context dismissed]; + [[delegate reject] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; +} + +- (void)testDismissedShouldCallDelegateOnDismiss { + CTTemplateContext *context = self.templateContext; + id delegate = OCMProtocolMock(@protocol(CTTemplateContextDismissDelegate)); + [context setDismissDelegate:delegate]; + [context dismissed]; + [[delegate verify] onDismissContext:[OCMArg any]]; + + // should call delegate dismiss only once + [context dismissed]; + [[delegate reject] onDismissContext:[OCMArg any]]; +} + +- (void)testPresentedShouldCallDelegateShow { + CTTemplateContext *context = self.templateContext; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context presented]; + [[delegate verify] notificationDidShow:[OCMArg any]]; +} + +- (void)testDismissClearsDelegate { + CTTemplateContext *context = self.templateContext; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context dismissed]; + [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; + + XCTAssertNil([context notificationDelegate]); + [context presented]; + [[delegate reject] notificationDidShow:[OCMArg any]]; +} + +- (void)testTriggerAction { + CTTemplateContext *context = self.templateContext; + CTInAppNotificationDisplayDelegateMock *delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeClose); + XCTAssertEqualObjects(extras[CLTAP_PROP_WZRK_CTA], @"map.actions.close"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.close"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeCustom); + XCTAssertEqualObjects(action.customTemplateInAppData.templateName, VARS_ACTION_FUNCTION_NAME); + XCTAssertEqualObjects(extras[CLTAP_PROP_WZRK_CTA], VARS_ACTION_FUNCTION_NAME); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.function"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeOpenURL); + XCTAssertEqualObjects(action.actionURL, [[NSURL alloc] initWithString:VARS_ACTION_OPEN_URL_ADDRESS]); + XCTAssertEqualObjects(extras[CLTAP_PROP_WZRK_CTA], @"map.actions.openUrl"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.openUrl"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeKeyValues); + XCTAssertEqualObjects(action.keyValues, @{ + @"key1": @"value1" + }); + XCTAssertEqualObjects(extras[CLTAP_PROP_WZRK_CTA], @"map.actions.kv"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.kv"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + // Action is called synchronously + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTFail(@"handleNotificationAction called for non-existent action arguments"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"nonexistent"]; + XCTAssertEqual(0, delegate.handleNotificationActionInvocations); +} + +- (void)testTriggerActionNOOPForFunction { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function notification:notification andFileDownloader:self.fileDownloader]; + CTInAppNotificationDisplayDelegateMock *delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"action"]; + XCTAssertEqual(0, delegate.handleNotificationActionInvocations); +} + +- (void)testDidShowNotCalledForActions { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function notification:notification andFileDownloader:self.fileDownloader]; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context presented]; + [[delegate reject] notificationDidShow:[OCMArg any]]; +} + +- (void)testDidDismissNotCalledForActionsNotVisual { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function notification:notification andFileDownloader:self.fileDownloader]; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + id dismissDelegate = OCMProtocolMock(@protocol(CTTemplateContextDismissDelegate)); + [context setNotificationDelegate:delegate]; + [context setDismissDelegate:dismissDelegate]; + [context dismissed]; + [[dismissDelegate verify] onDismissContext:[OCMArg any]]; + [[delegate reject] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; +} + +- (void)testDidDismissCalledForActionsVisual { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.functionVisual notification:notification andFileDownloader:self.fileDownloader]; + + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context dismissed]; + [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; +} + +- (void)testTemplateName { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.templateNotificationJson]; + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.template notification:notification andFileDownloader:self.fileDownloader]; + XCTAssertEqualObjects(TEMPLATE_NAME_NESTED, [context templateName]); +} + +- (void)testSimpleValueOverrides { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.simpleTemplateNotificationJson]; + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.simpleTemplate notification:notification andFileDownloader:self.fileDownloader]; + XCTAssertEqual(VARS_OVERRIDE_BOOLEAN, [context boolNamed:@"a.b.c"]); + XCTAssertEqualObjects(VARS_OVERRIDE_STRING, [context stringNamed:@"a.b.d"]); + XCTAssertEqualObjects(@(VARS_OVERRIDE_LONG), [context numberNamed:@"a.b.e.f"]); + XCTAssertEqual(VARS_OVERRIDE_DOUBLE, [context doubleNamed:@"g"]); + XCTAssertEqual(VARS_OVERRIDE_BOOLEAN, [context boolNamed:@"h"]); + XCTAssertEqualObjects(VARS_DEFAULT_STRING, [context stringNamed:@"i"]); + + NSDictionary *a = [context dictionaryNamed:@"a"]; + XCTAssertTrue([a isEqualToDictionary:(@{ + @"b": @{ + @"c": @(VARS_OVERRIDE_BOOLEAN), + @"d": VARS_OVERRIDE_STRING, + @"e": @{ + @"f": @(VARS_OVERRIDE_LONG) + } + } + })]); + + NSDictionary *ab = [context dictionaryNamed:@"a.b"]; + XCTAssertTrue([ab isEqualToDictionary:(@{ + @"c": @(VARS_OVERRIDE_BOOLEAN), + @"d": VARS_OVERRIDE_STRING, + @"e": @{ + @"f": @(VARS_OVERRIDE_LONG) + } + })]); + + NSDictionary *abe = [context dictionaryNamed:@"a.b.e"]; + XCTAssertTrue([abe isEqualToDictionary:(@{ + @"f": @(VARS_OVERRIDE_LONG) + })]); + + NSDictionary *abg = [context dictionaryNamed:@"a.b.g"]; + XCTAssertNil(abg); +} + +- (void)testValueOverrides { + XCTAssertEqual(VARS_OVERRIDE_BOOLEAN, [self.templateContext boolNamed:@"boolean"]); + XCTAssertEqual(VARS_OVERRIDE_STRING, [self.templateContext stringNamed:@"string"]); + XCTAssertEqual(VARS_OVERRIDE_CHAR, [self.templateContext charNamed:@"char"]); + XCTAssertEqual(VARS_OVERRIDE_LONG, [self.templateContext longNamed:@"long"]); + XCTAssertEqual(VARS_OVERRIDE_DOUBLE, [self.templateContext doubleNamed:@"double"]); + XCTAssertEqualObjects(@(VARS_DEFAULT_INT), [self.templateContext numberNamed:@"noOverrideInt"]); + XCTAssertFalse([self.templateContext boolNamed:@"overrideWithoutDefinitionBoolean"]); + XCTAssertNil([self.templateContext numberNamed:@"nonDefinedNumber"]); +} + +- (void)testNotDefinedValues { + XCTAssertFalse([self.templateContext boolNamed:@"overrideWithoutDefinitionBoolean"]); + XCTAssertNil([self.templateContext stringNamed:@"notDefinedString"]); + XCTAssertNil([self.templateContext numberNamed:@"notDefinedNumber"]); + XCTAssertNil([self.templateContext dictionaryNamed:@"notDefinedMap"]); + XCTAssertEqual(0, [self.templateContext longNamed:@"notDefinedLong"]); + XCTAssertEqual(0, [self.templateContext charNamed:@"notDefinedChar"]); + XCTAssertEqual(0, [self.templateContext intNamed:@"notDefinedInt"]); + XCTAssertEqual(0, [self.templateContext doubleNamed:@"notDefinedDouble"]); + XCTAssertEqual(0, [self.templateContext floatNamed:@"notDefinedFloat"]); +} + +- (void)testDictionaryArguments { + NSDictionary *notificationVars = self.templateNotificationJson[@"vars"]; + + NSDictionary *map = [self.templateContext dictionaryNamed:@"map"]; + XCTAssertEqualObjects(notificationVars[@"map.int"], map[@"int"]); + XCTAssertEqualObjects(notificationVars[@"map.float"], map[@"float"]); + XCTAssertEqualObjects(@25, map[@"noOverrideInt"]); + + NSDictionary *innerMap = map[@"innerMap"]; + [self verifyInnerMap:notificationVars map:innerMap]; + XCTAssertTrue([innerMap isEqualToDictionary:[self.templateContext dictionaryNamed:@"map.innerMap"]]); + + NSDictionary *innermostMap = innerMap[@"innermostMap"]; + [self verifyInnermostMap:notificationVars map:innermostMap]; + XCTAssertTrue([innermostMap isEqualToDictionary:[self.templateContext dictionaryNamed:@"map.innerMap.innermostMap"]]); +} + +- (void)testActionsValueInDictionary { + NSDictionary *actionsMap = [self.templateContext dictionaryNamed:@"map.actions"]; + XCTAssertEqualObjects(VARS_ACTION_FUNCTION_NAME, actionsMap[@"function"]); + XCTAssertEqualObjects(@"close", actionsMap[@"close"]); +} + +- (void)testFileArgument { + XCTAssertEqualObjects(VARS_FILE_URL, [self.templateContext fileNamed:@"map.file"]); + XCTAssertEqualObjects(VARS_IMAGE_URL, [self.templateContext fileNamed:@"file"]); + XCTAssertNil([self.templateContext fileNamed:@"noOverrideFile"]); +} + +- (void)verifyInnerMap:(NSDictionary *)vars map:(NSDictionary *)map { + XCTAssertEqualObjects(vars[@"map.innerMap.boolean"], map[@"boolean"]); + XCTAssertEqualObjects(vars[@"map.innerMap.string"], map[@"string"]); + XCTAssertEqualObjects(vars[@"map.innerMap.char"], map[@"char"]); + XCTAssertEqualObjects(vars[@"map.innerMap.int"], map[@"int"]); + XCTAssertEqualObjects(vars[@"map.innerMap.long"], map[@"long"]); + XCTAssertEqualObjects(vars[@"map.innerMap.double"], map[@"double"]); + XCTAssertEqualObjects(@15, map[@"noOverrideInt"]); +} + +- (void)verifyInnermostMap:(NSDictionary *)vars map:(NSDictionary *)map { + XCTAssertEqualObjects(vars[@"map.innerMap.innermostMap.int"], map[@"int"]); + XCTAssertEqualObjects(vars[@"map.innerMap.innermostMap.string"], map[@"string"]); + XCTAssertEqualObjects(vars[@"map.innerMap.innermostMap.boolean"], map[@"boolean"]); + XCTAssertEqualObjects(@YES, map[@"noOverrideBoolean"]); +} + +- (CTCustomTemplate *)simpleTemplate { + CTInAppTemplateBuilder *builder = [[CTInAppTemplateBuilder alloc] init]; + [builder setName:SIMPLE_TEMPLATE_NAME]; + [builder addArgument:@"a.b.c" withBool:VARS_DEFAULT_BOOLEAN]; + [builder addArgument:@"a.b.d" withString:VARS_DEFAULT_STRING]; + [builder addArgument:@"a.b.e.f" withNumber:@(VARS_DEFAULT_LONG)]; + [builder addArgument:@"g" withNumber:@(VARS_DEFAULT_DOUBLE)]; + [builder addArgument:@"h" withBool:VARS_DEFAULT_BOOLEAN]; + [builder addArgument:@"i" withString:VARS_DEFAULT_STRING]; + [builder setPresenter:[CTTemplatePresenterMock new]]; + return [builder build]; +} + +- (NSDictionary *)simpleTemplateNotificationJson { + return @{ + @"templateName": SIMPLE_TEMPLATE_NAME, + @"type": @"custom-code", + @"vars": @{ + @"a.b.c": @(VARS_OVERRIDE_BOOLEAN), + @"a.b.d": VARS_OVERRIDE_STRING, + @"a.b.e.f": @(VARS_OVERRIDE_LONG), + @"g": @(VARS_OVERRIDE_DOUBLE), + @"h": @YES + } + }; +} + +- (CTTemplateContext *)templateContext { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.templateNotificationJson]; + return [[CTTemplateContext alloc] initWithTemplate:self.template notification:notification andFileDownloader:self.fileDownloader]; +} + +- (CTCustomTemplate *)template { + CTInAppTemplateBuilder *templateBuilder = [[CTInAppTemplateBuilder alloc] init]; + [templateBuilder setName:TEMPLATE_NAME_NESTED]; + [templateBuilder addArgument:@"boolean" withBool:NO]; + [templateBuilder addArgument:@"char" withNumber:[NSNumber numberWithChar:VARS_DEFAULT_CHAR]]; + [templateBuilder addArgument:@"string" withString:VARS_DEFAULT_STRING]; + [templateBuilder addArgument:@"long" withNumber:[NSNumber numberWithLong:VARS_DEFAULT_LONG]]; + [templateBuilder addArgument:@"double" withNumber:@(VARS_DEFAULT_DOUBLE)]; + [templateBuilder addArgument:@"map.int" withNumber:@(VARS_DEFAULT_INT)]; + [templateBuilder addArgument:@"noOverrideInt" withNumber:@(VARS_DEFAULT_INT)]; + [templateBuilder addArgument:@"map.noOverrideInt" withNumber:@25]; + [templateBuilder addArgument:@"map" withDictionary:@{ + @"float": @15.6f, + @"innerMap": @{ + @"boolean": @NO, + @"string": @"Default", + @"noOverrideInt": @15 + } + }]; + [templateBuilder addArgument:@"map.innerMap" withDictionary:@{ + @"char": @10, + @"int": @1100, + @"long": @21474836472, + @"innermostMap": @{ + @"int": @1200, + @"string": @"Default", + @"boolean": @NO, + @"noOverrideBoolean": @YES + } + }]; + [templateBuilder addArgument:@"map.innerMap.double" withNumber:@12.12]; + [templateBuilder addActionArgument:@"map.actions.function"]; + [templateBuilder addActionArgument:@"map.actions.close"]; + [templateBuilder addActionArgument:@"map.actions.openUrl"]; + [templateBuilder addActionArgument:@"map.actions.kv"]; + [templateBuilder addFileArgument:@"map.file"]; + [templateBuilder addFileArgument:@"file"]; + [templateBuilder addFileArgument:@"noOverrideFile"]; + [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; + return [templateBuilder build]; +} + +- (NSDictionary *)templateNotificationJson { + return @{ + @"templateName": TEMPLATE_NAME_NESTED, + @"type": @"custom-code", + @"vars": @{ + @"boolean": @(VARS_OVERRIDE_BOOLEAN), + @"string": VARS_OVERRIDE_STRING, + @"char": @(VARS_OVERRIDE_CHAR), + @"long": @(VARS_OVERRIDE_LONG), + @"double": @(VARS_OVERRIDE_DOUBLE), + @"overrideWithoutDefinitionBoolean": @YES, + @"map.actions.close": @{ + @"actions": @{ + @"type": @"close" + } + }, + @"map.actions.function": @{ + @"actions": @{ + @"templateName": VARS_ACTION_FUNCTION_NAME, + @"type": @"custom-code", + @"vars": @{ + @"boolean": @(VARS_ACTION_OVERRIDE_BOOLEAN), + @"string": VARS_ACTION_OVERRIDE_STRING, + @"int": @(VARS_ACTION_OVERRIDE_INT) + } + } + }, + @"map.actions.openUrl": @{ + @"actions": @{ + @"type": @"url", + @"ios": VARS_ACTION_OPEN_URL_ADDRESS + } + }, + @"map.actions.kv": @{ + @"actions": @{ + @"type": @"kv", + @"kv": @{ + @"key1": @"value1" + }, + } + }, + @"map.int": @123, + @"map.float": @15.6f, + @"map.innerMap.boolean": @YES, + @"map.innerMap.string": @"String", + @"map.innerMap.char": @1, + @"map.innerMap.int": @1345, + @"map.innerMap.long": @21474836470, + @"map.innerMap.double": @3402823466385288.0, + @"map.innerMap.innermostMap.int": @1024, + @"map.innerMap.innermostMap.string": @"innerText", + @"map.innerMap.innermostMap.boolean": @YES, + @"map.file": VARS_FILE_URL, + @"file": VARS_IMAGE_URL, + } + }; +} + +- (CTCustomTemplate *)function { + CTAppFunctionBuilder *bulder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [bulder setName:FUNCTION_NAME]; + [bulder addArgument:@"string" withString:VARS_DEFAULT_STRING]; + [bulder setPresenter:[CTTemplatePresenterMock new]]; + return [bulder build]; +} + +- (CTCustomTemplate *)functionVisual { + CTAppFunctionBuilder *bulder = [[CTAppFunctionBuilder alloc] initWithIsVisual:YES]; + [bulder setName:FUNCTION_NAME]; + [bulder addArgument:@"string" withString:VARS_DEFAULT_STRING]; + [bulder setPresenter:[CTTemplatePresenterMock new]]; + return [bulder build]; +} + +- (NSDictionary *)functionNotificationJson { + return @{ + @"templateName": FUNCTION_NAME, + @"type": @"custom-code", + @"vars": @{ + @"string": VARS_OVERRIDE_STRING + } + }; +} + +static NSString * const SIMPLE_TEMPLATE_NAME = @"Template"; +static NSString * const TEMPLATE_NAME_NESTED = @"TemplateNestedArgs"; +static NSString * const FUNCTION_NAME = @"Function"; + +static BOOL const VARS_OVERRIDE_BOOLEAN = YES; +static NSString * const VARS_OVERRIDE_STRING = @"Text"; +static char const VARS_OVERRIDE_CHAR = 10; +static long long const VARS_OVERRIDE_LONG = 21474836475; +static double const VARS_OVERRIDE_DOUBLE = 3402823466385285.0; + +static BOOL const VARS_DEFAULT_BOOLEAN = NO; +static NSString * const VARS_DEFAULT_STRING = @"Default"; +static char const VARS_DEFAULT_CHAR = 1; +static long long const VARS_DEFAULT_LONG = 5435050l; +static double const VARS_DEFAULT_DOUBLE = 12.345678; +static int const VARS_DEFAULT_INT = 35; + +static NSString * const VARS_ACTION_FUNCTION_NAME = @"function"; +static BOOL const VARS_ACTION_OVERRIDE_BOOLEAN = YES; +static NSString * const VARS_ACTION_OVERRIDE_STRING = @"Function text"; +static int const VARS_ACTION_OVERRIDE_INT = 5421; + +static NSString * const VARS_ACTION_OPEN_URL_ADDRESS = @"https://clevertap.com"; +static NSString * const VARS_FILE_URL = @"https://clevertap.com/file.pdf"; +static NSString * const VARS_IMAGE_URL = @"https://clevertap.com/image.png"; + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h new file mode 100644 index 00000000..0e7bb9cc --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h @@ -0,0 +1,24 @@ +// +// CTTemplatePresenterMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTTemplatePresenter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTTemplatePresenterMock : NSObject + +@property (nonatomic) int onCloseInvocationsCount; +@property (nonatomic) CTTemplateContext *onCloseContext; + +@property (nonatomic) int onPresentInvocationsCount; +@property (nonatomic) CTTemplateContext *onPresentContext; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m new file mode 100644 index 00000000..db7dad78 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m @@ -0,0 +1,23 @@ +// +// CTTemplatePresenterMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 10.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTTemplatePresenterMock.h" + +@implementation CTTemplatePresenterMock + +- (void)onCloseClicked:(CTTemplateContext *)context { + self.onCloseInvocationsCount++; + self.onCloseContext = context; +} + +- (void)onPresent:(CTTemplateContext *)context { + self.onPresentInvocationsCount++; + self.onPresentContext = context; +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.h b/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.h new file mode 100644 index 00000000..363ab28f --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.h @@ -0,0 +1,24 @@ +// +// CTTestTemplateProducer.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 11.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTTemplateProducer.h" +#import "CTCustomTemplate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTTestTemplateProducer : NSObject + +@property (nonatomic, strong) NSSet *templates; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithTemplates:(NSSet *)templates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.m new file mode 100644 index 00000000..7b355e51 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTestTemplateProducer.m @@ -0,0 +1,24 @@ +// +// CTTestTemplateProducer.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 11.03.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTTestTemplateProducer.h" + +@implementation CTTestTemplateProducer + +- (instancetype)initWithTemplates:(NSSet *)templates { + if (self = [super init]) { + _templates = templates; + } + return self; +} + +- (NSSet *)defineTemplates:(NSString *)accountId { + return self.templates; +} + +@end diff --git a/CleverTapSDKTests/InApps/InAppHelper.h b/CleverTapSDKTests/InApps/InAppHelper.h index 196f71d1..0a782204 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.h +++ b/CleverTapSDKTests/InApps/InAppHelper.h @@ -15,7 +15,7 @@ @class CleverTapInstanceConfig; @class CTMultiDelegateManager; @class CTInAppTriggerManager; -@class CTInAppImagePrefetchManager; +@class CTFileDownloader; NS_ASSUME_NONNULL_BEGIN @@ -34,7 +34,7 @@ extern NSString *const CLTAP_TEST_CAMPAIGN_ID; @property (nonatomic, strong) CTImpressionManager *impressionManager; @property (nonatomic, strong) CTInAppStore *inAppStore; @property (nonatomic, strong) CTInAppTriggerManager *inAppTriggerManager; -@property (nonatomic, strong) CTInAppImagePrefetchManager *imagePrefetchManager; +@property (nonatomic, strong) CTFileDownloader *fileDownloader; - (NSString *)accountId; - (NSString *)accountToken; diff --git a/CleverTapSDKTests/InApps/InAppHelper.m b/CleverTapSDKTests/InApps/InAppHelper.m index 1b38c358..c523ad61 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.m +++ b/CleverTapSDKTests/InApps/InAppHelper.m @@ -15,7 +15,7 @@ #import "CleverTapInstanceConfig.h" #import "CTInAppFCManager.h" #import "CTInAppTriggerManager.h" -#import "CTInAppImagePrefetchManager.h" +#import "CTFileDownloader.h" NSString *const CLTAP_TEST_ACCOUNT_ID = @"testAccountId"; NSString *const CLTAP_TEST_ACCOUNT_TOKEN = @"testAccountToken"; @@ -47,7 +47,7 @@ - (instancetype)init { self.config = [[CleverTapInstanceConfig alloc] initWithAccountId:self.accountId accountToken:self.accountToken]; - self.imagePrefetchManager = [[CTInAppImagePrefetchManager alloc] initWithConfig:self.config]; + self.fileDownloader = [[CTFileDownloader alloc] initWithConfig:self.config]; self.impressionManager = [[CTImpressionManager alloc] initWithAccountId:self.accountId deviceId:self.deviceId @@ -55,7 +55,6 @@ - (instancetype)init { self.inAppStore = [[CTInAppStore alloc] initWithConfig:self.config delegateManager:self.delegateManager - imagePrefetchManager:self.imagePrefetchManager deviceId:self.deviceId]; self.inAppTriggerManager = [[CTInAppTriggerManager alloc] initWithAccountId:self.accountId diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h index a2c93354..1e37d0e6 100644 --- a/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h +++ b/CleverTapSDKTests/ProductExperiences/CTVarCache+Tests.h @@ -11,10 +11,14 @@ NS_ASSUME_NONNULL_BEGIN @interface CTVarCache (Tests) + @property (strong, nonatomic) NSMutableDictionary *valuesFromClient; +@property (strong, nonatomic) id merged; + - (NSString*)dataArchiveFileName; - (id)traverse:(id)collection withKey:(id)key autoInsert:(BOOL)autoInsert; - (void)saveDiffs; + @end NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m b/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m index 13055a6c..bba81518 100644 --- a/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m +++ b/CleverTapSDKTests/ProductExperiences/CTVarCacheTest.m @@ -15,9 +15,15 @@ #import "CTVarCacheMock.h" #import "CTVariables+Tests.h" #import "CTConstants.h" +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" +#import "CTFileDownloadTestHelper.h" @interface CTVarCacheTest : XCTestCase +@property (nonatomic, strong) CTFileDownloaderMock *fileDownloader; +@property (nonatomic, strong) CTFileDownloadTestHelper *fileDownloadHelper; + @end CTVariables *variables; @@ -28,12 +34,18 @@ - (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.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:config]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo fileDownloader:self.fileDownloader]; variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; + + self.fileDownloadHelper = [CTFileDownloadTestHelper new]; + [self.fileDownloadHelper addHTTPStub]; } - (void)tearDown { variables = nil; + [self.fileDownloadHelper removeStub]; + [self.fileDownloadHelper cleanUpFiles:self.fileDownloader forTest:self]; } #pragma mark Name Components @@ -449,4 +461,155 @@ - (void)deleteSavedFile:(NSString *)fileName { [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; } +#pragma mark - File type vars tests + +- (void)testRegisterFileVars { + CTVar *var = [variables define:@"test" with:nil kind:CT_KIND_FILE]; + XCTAssertEqual(variables.varCache.vars[var.name], var); +} + +- (void)testGetFileVariable { + NSString *varName = @"var"; + CTVar *var = [variables define:varName with:nil kind:CT_KIND_FILE]; + CTVar *varResult = [variables.varCache getVariable:varName]; + + XCTAssertEqual(varResult, var); +} + +- (void)testFileVarApplyDiffs { + NSArray *urls = [self.fileDownloadHelper generateFileURLStrings:2]; + + // Register Vars + CTVar *var1 = [variables define:@"var1" with:nil kind:CT_KIND_FILE]; + CTVar *group1_var1 = [variables define:@"group1.var1" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + XCTAssertNotNil([status objectForKey:urls[0]]); + XCTAssertNotNil([status objectForKey:urls[1]]); + [expectation fulfill]; + }; + + // Apply diffs + NSDictionary *diffs = @{ + @"var1": urls[0], + @"group1": @{ + @"var1": urls[1] + } + }; + // File vars value should be nil when not downloaded/present + XCTAssertEqualObjects(nil, var1.value); + XCTAssertEqualObjects(nil, group1_var1.value); + + [variables.varCache applyVariableDiffs:diffs]; + [self waitForExpectations:@[expectation] timeout:2.0]; + + // File var value should be file downloaded path + NSString *expValue1 = [self.fileDownloader fileDownloadPath:urls[0]]; + NSString *expValue2 = [self.fileDownloader fileDownloadPath:urls[1]]; + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue2, group1_var1.value); +} + +- (void)testFileVariableValues { + NSString *url = [self.fileDownloadHelper generateFileURLString]; + + // Register Vars + CTVar *var1 = [variables define:@"var1" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + + // Apply diffs + NSDictionary *diffs = @{ + @"var1": url + }; + [variables.varCache applyVariableDiffs:diffs]; + [self waitForExpectations:@[expectation] timeout:2.0]; + + NSString *expValue1 = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(url, var1.fileURL); + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue1, var1.stringValue); + XCTAssertEqualObjects(expValue1, var1.fileValue); +} + +- (void)testApplyVariableValuesNil { + NSString *url = [self.fileDownloadHelper generateFileURLString]; + + // Register Vars + CTVar *var1 = [variables define:@"var1" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + + // Apply diffs with var1 override + NSDictionary *diffs = @{ + @"var1": url + }; + [variables.varCache applyVariableDiffs:diffs]; + [self waitForExpectations:@[expectation] timeout:2.0]; + + NSString *expValue1 = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(url, var1.fileURL); + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue1, var1.stringValue); + XCTAssertEqualObjects(expValue1, var1.fileValue); + + // Apply diffs with no override + NSDictionary *diffsNil = @{ + }; + [variables.varCache applyVariableDiffs:diffsNil]; + XCTAssertNil(var1.fileURL); + XCTAssertNil(var1.value); + XCTAssertNil(var1.stringValue); + XCTAssertNil(var1.fileValue); +} + +- (void)testDefineFileVarAfterResponse { + NSString *url = [self.fileDownloadHelper generateFileURLString]; + // Apply diffs with var1 override + NSDictionary *diffs = @{ + @"var1": url + }; + [variables handleVariablesResponse:diffs]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + // Define the variable after the initial response and applied diffs + CTVar *var1 = [variables define:@"var1" with:nil kind:CT_KIND_FILE]; + [self waitForExpectations:@[expectation] timeout:2.0]; + + NSString *expValue1 = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(url, var1.fileURL); + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue1, var1.stringValue); + XCTAssertEqualObjects(expValue1, var1.fileValue); +} + +- (void)testFileVarUpdated { + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + NSString *url = [self.fileDownloadHelper generateFileURLString]; + variables.varCache.merged = [NSMutableDictionary dictionaryWithDictionary:@{ + @"var1": url + }]; + CTVar *var1 = [variables define:@"var1" with:nil kind:CT_KIND_FILE]; + [var1 onFileIsReady:^{ + NSString *expValue1 = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(url, var1.fileURL); + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue1, var1.stringValue); + XCTAssertEqualObjects(expValue1, var1.fileValue); + [expectation fulfill]; + }]; + [variables.varCache fileVarUpdated:var1]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + @end diff --git a/CleverTapSDKTests/ProductExperiences/CTVarTest.m b/CleverTapSDKTests/ProductExperiences/CTVarTest.m index c02777df..45950a76 100644 --- a/CleverTapSDKTests/ProductExperiences/CTVarTest.m +++ b/CleverTapSDKTests/ProductExperiences/CTVarTest.m @@ -8,16 +8,23 @@ #import #import +#import "CTPreferences.h" #import "CTVariables.h" #import "CTVarCache.h" #import "CTVarCacheMock.h" #import "CTConstants.h" #import "CTVariables+Tests.h" +#import "CTVarCache+Tests.h" +#import "CTVar-Internal.h" +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" +#import "CTFileDownloadTestHelper.h" @interface CTVarDelegateImpl : NSObject typedef void(^Callback)(CTVar *); @property Callback callback; +@property Callback fileReadyCallback; @end @@ -29,11 +36,19 @@ - (void)valueDidChange:(CTVar *)variable { } } +- (void)fileIsReady:(CTVar *)var { + if ([self fileReadyCallback]) { + self.fileReadyCallback(var); + } +} + @end @interface CTVarTest : XCTestCase @property(strong, nonatomic) CTVariables *variables; +@property (nonatomic, strong) CTFileDownloaderMock *fileDownloader; +@property (nonatomic, strong) CTFileDownloadTestHelper *fileDownloadHelper; @end @@ -43,12 +58,18 @@ - (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.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:config]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo fileDownloader:self.fileDownloader]; self.variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; + + self.fileDownloadHelper = [CTFileDownloadTestHelper new]; + [self.fileDownloadHelper addHTTPStub]; } - (void)tearDown { self.variables = nil; + [self.fileDownloadHelper removeStub]; + [self.fileDownloadHelper cleanUpFiles:self.fileDownloader forTest:self]; } - (void)testVariableName { @@ -225,4 +246,179 @@ - (void)testVarValues { XCTAssertTrue([groupVar.defaultValue isKindOfClass:[NSDictionary class]]); } +#pragma mark - File type vars tests + +- (void)testDefineFileVariable { + CTVar *var = [self.variables define:@"fileVar" with:nil kind:CT_KIND_FILE]; + + XCTAssertEqualObjects(@"fileVar", var.name); + XCTAssertEqualObjects(CT_KIND_FILE, var.kind); + XCTAssertNil(var.value); + XCTAssertNil(var.stringValue); + XCTAssertNil(var.fileValue); +} + +- (void)testCTVarDelegateFileIsReady { + // Register File var + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; + __block CTVar *varFromDelegate = nil; + [del setFileReadyCallback:^(CTVar * variable) { + varFromDelegate = variable; + [expect fulfill]; + }]; + [var1 setDelegate:del]; + + // Apply diffs + NSString *url = [self.fileDownloadHelper generateFileURLString]; + NSDictionary *diffs = @{ + @"var1": url + }; + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + + NSString *expValue = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(@"var1", varFromDelegate.name); + XCTAssertEqualObjects(expValue, varFromDelegate.value); +} + +- (void)testOnFileIsReady { + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expect = [self expectationWithDescription:@"onFileIsReady"]; + [var1 onFileIsReady:^{ + [expect fulfill]; + }]; + + NSString *url = [self.fileDownloadHelper generateFileURLString]; + NSDictionary *diffs = @{ + @"var1": url + }; + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testFileVarUpdate { + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + NSString *url = [self.fileDownloadHelper generateFileURLString]; + self.variables.varCache.merged = [NSMutableDictionary dictionaryWithDictionary:@{ + @"var1": url + }]; + XCTAssertTrue([var1 update]); + XCTAssertFalse(var1.hadStarted); + XCTAssertFalse([var1 update]); + + self.variables.varCache.merged = [NSMutableDictionary dictionaryWithDictionary:@{ + @"var1": [NSString stringWithFormat:@"%@?changed", url] + }]; + XCTAssertTrue([var1 update]); + XCTAssertFalse(var1.hadStarted); + XCTAssertFalse([var1 update]); +} + +- (void)testOnFileIsReadyNoOverride { + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + XCTestExpectation *expect = [self expectationWithDescription:@"onFileIsReady"]; + XCTestExpectation *expect1 = [self expectationWithDescription:@"onFileIsReady After Change"]; + __block int count = 0; + [var1 onFileIsReady:^{ + count++; + if (count == 1) { + [expect fulfill]; + } else { + [expect1 fulfill]; + } + }]; + + NSString *url = [self.fileDownloadHelper generateFileURLString]; + NSDictionary *diffs = @{ + @"var1": url + }; + [self.variables.varCache applyVariableDiffs:diffs]; + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + + [self.variables handleVariablesResponse:@{}]; + [self waitForExpectations:@[expect1] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testFileVariablesCallbacks { + NSString *url = [self.fileDownloadHelper generateFileURLString]; + + // Register Vars + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + CTVar *var2 = [self.variables define:@"var2" with:nil kind:CT_KIND_FILE]; + + XCTestExpectation *expect1 = [self expectationWithDescription:@"var1 onValueChanged"]; + [var1 onValueChanged:^{ + XCTAssertEqualObjects(url, var1.fileURL); + [expect1 fulfill]; + }]; + + XCTestExpectation *expect2 = [self expectationWithDescription:@"var2 onValueChanged"]; + [var2 onValueChanged:^{ + XCTAssertNil(var2.value); + [expect2 fulfill]; + }]; + + XCTestExpectation *expect3 = [self expectationWithDescription:@"var1 onFileIsReady"]; + [var1 onFileIsReady:^{ + NSString *expValue1 = [self.fileDownloader fileDownloadPath:url]; + XCTAssertEqualObjects(expValue1, var1.value); + XCTAssertEqualObjects(expValue1, var1.stringValue); + XCTAssertEqualObjects(expValue1, var1.fileValue); + [expect3 fulfill]; + }]; + + [var2 onFileIsReady:^{ + XCTAssertNil(var2.value); + XCTAssertNil(var2.fileValue); + }]; + + XCTestExpectation *expectationDownload = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + [expectationDownload fulfill]; + }; + + NSDictionary *diffs = @{ + @"var1": url + }; + [self.variables handleVariablesResponse:diffs]; + [self waitForExpectations:@[expectationDownload, expect1, expect2, expect3] timeout:2.0]; +} + +- (void)testCallbacksDefineFileVarAfterResponse { + NSString *url = [self.fileDownloadHelper generateFileURLString]; + // Apply diffs with var1 override + NSDictionary *diffs = @{ + @"var1": url + }; + [self.variables handleVariablesResponse:diffs]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for download completion"]; + self.fileDownloader.downloadCompletion = ^(NSDictionary * _Nonnull status) { + [expectation fulfill]; + }; + + // Create delegate + CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; + XCTestExpectation *expectationDelegate = [self expectationWithDescription:@"FileReadyCallback completion"]; + XCTestExpectation *expectationBlock = [self expectationWithDescription:@"FileReadyCallback completion"]; + [del setFileReadyCallback:^(CTVar * variable) { + [expectationDelegate fulfill]; + }]; + // Define the variable after the initial response and applied diffs + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + // Set delegate + [var1 setDelegate:del]; + // Set block + [var1 onFileIsReady:^{ + [expectationBlock fulfill]; + }]; + [self waitForExpectations:@[expectation, expectationDelegate, expectationBlock] timeout:2.0]; +} + @end diff --git a/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m index 0542d2c8..3b3b87a6 100644 --- a/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m +++ b/CleverTapSDKTests/ProductExperiences/CTVariables+Tests.m @@ -15,6 +15,7 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config deviceInfo: (CT self = [super init]; if (self) { self.varCache = varCache; + [self.varCache setDelegate:self]; } return self; } diff --git a/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m b/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m index 1ae58173..a30414f2 100644 --- a/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m +++ b/CleverTapSDKTests/ProductExperiences/CTVariablesTest.m @@ -11,11 +11,17 @@ #import "CTVariables+Tests.h" #import "CTVarCacheMock.h" #import "CTVariables.h" +#import "CTPreferences.h" #import "CTConstants.h" +#import "CTFileDownloaderMock.h" +#import "CTFileDownloader+Tests.h" +#import "CTFileDownloadTestHelper.h" @interface CTVariablesTest : XCTestCase @property(strong, nonatomic) CTVariables *variables; +@property (nonatomic, strong) CTFileDownloaderMock *fileDownloader; +@property (nonatomic, strong) CTFileDownloadTestHelper *fileDownloadHelper; @end @@ -24,12 +30,18 @@ @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.fileDownloader = [[CTFileDownloaderMock alloc] initWithConfig:config]; + CTVarCacheMock *varCache = [[CTVarCacheMock alloc] initWithConfig:config deviceInfo:deviceInfo fileDownloader:self.fileDownloader]; self.variables = [[CTVariables alloc] initWithConfig:config deviceInfo:deviceInfo varCache:varCache]; + + self.fileDownloadHelper = [CTFileDownloadTestHelper new]; + [self.fileDownloadHelper addHTTPStub]; } - (void)tearDown { self.variables = nil; + [self.fileDownloadHelper removeStub]; + [self.fileDownloadHelper cleanUpFiles:self.fileDownloader forTest:self]; } - (void)testVarCacheNotNil { @@ -575,5 +587,56 @@ - (void)testFetchVariablesOnError { [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; } +#pragma mark - File type vars tests + +- (void)testOnceVariablesChangedAndNoDownloadsPending { + __block int count = 0; + [self.variables onceVariablesChangedAndNoDownloadsPending:^{ + // Should be called once + count++; + }]; + + [self.variables triggerNoDownloadsPending]; + [self.variables triggerNoDownloadsPending]; + XCTAssertEqual(count, 1); +} + +- (void)testOnVariablesChangedAndNoDownloadsPending { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + XCTestExpectation *expect2 = [self expectationWithDescription:@"delegate2"]; + + [self.variables onVariablesChangedAndNoDownloadsPending:^{ + [expect fulfill]; + }]; + [self.variables onVariablesChangedAndNoDownloadsPending:^{ + [expect2 fulfill]; + }]; + CTVar *var1 = [self.variables define:@"var1" with:nil kind:CT_KIND_FILE]; + NSString *url = [self.fileDownloadHelper generateFileURLString]; + NSDictionary *diffs = @{ + @"var1": url + }; + XCTAssertEqualObjects(nil, var1.value); + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect, expect2] timeout:DISPATCH_TIME_NOW + 5.0]; +} + +- (void)testNoDownloadsPendingCallbackWhenNoFileNeedsDownload { + XCTestExpectation *expect = [self expectationWithDescription:@"delegate"]; + + [self.variables onVariablesChangedAndNoDownloadsPending:^{ + [expect fulfill]; + }]; + CTVar *var1 = [self.variables define:@"var1" with:@1 kind:CT_KIND_INT]; + NSDictionary *diffs = @{ + @"var1": @1, + }; + [self.variables handleVariablesResponse:diffs]; + + [self waitForExpectations:@[expect] timeout:DISPATCH_TIME_NOW + 5.0]; + XCTAssertEqualObjects(@1, var1.value); +} + @end diff --git a/CleverTapSDKTests/Stub Responses/samplePDFStub.pdf b/CleverTapSDKTests/Stub Responses/samplePDFStub.pdf new file mode 100644 index 00000000..774c2ea7 Binary files /dev/null and b/CleverTapSDKTests/Stub Responses/samplePDFStub.pdf differ diff --git a/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt b/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt new file mode 100644 index 00000000..552c384a --- /dev/null +++ b/CleverTapSDKTests/Stub Responses/sampleTXTStub.txt @@ -0,0 +1 @@ +This is sample text file diff --git a/Package.swift b/Package.swift index af5cc91c..c8d9b055 100644 --- a/Package.swift +++ b/Package.swift @@ -48,6 +48,7 @@ let package = Package( .headerSearchPath("ProductConfig/controllers"), .headerSearchPath("InApps/"), .headerSearchPath("InApps/Matchers/"), + .headerSearchPath("InApps/CustomTemplates/"), .headerSearchPath("Inbox/"), .headerSearchPath("Inbox/cells"), .headerSearchPath("Inbox/config"), @@ -56,7 +57,8 @@ let package = Package( .headerSearchPath("Inbox/views"), .headerSearchPath("ProductExperiences/"), .headerSearchPath("Session/"), - .headerSearchPath("Swizzling/") + .headerSearchPath("Swizzling/"), + .headerSearchPath("FileDownload/") ], linkerSettings: [ .linkedFramework("AVFoundation"), diff --git a/README.md b/README.md index 48fa2bd3..29597608 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,17 @@ For more details, refer to our [Advanced iOS Push Notifications](https://develop CleverTap Geofence SDK provides Geofencing capabilities to CleverTap iOS SDK. To find the installation & integration steps for CleverTap Geofence SDK, click [here](https://github.com/CleverTap/clevertap-geofence-ios). -## Push Primer +## 📲 Push Primer -CleverTap iOS SDK supports Push Primer for push notification runtime permission, refer [Push Primer doc](/docs/PushPrimer.md) for more details. +CleverTap iOS SDK supports Push Primer for push notification runtime permission, refer to [Push Primer](/docs/PushPrimer.md) for more details. -## Remote Config Variables +## #️⃣ Remote Config Variables -CleverTap iOS SDK supports creating remote config variables, refer [Remote Config Variables](/docs/Variables.md) for more details and usage examples. +CleverTap iOS SDK supports creating remote config variables, refer to [Remote Config Variables](/docs/Variables.md) for more details and usage examples. + +## 🕹️ Custom Code Templates + +CleverTap iOS SDK supports creating Custom Code Templates for in-app notifications, refer to [Custom Code Templates](/docs/CustomCodeTemplates.md) for more details and usage examples. ## 𝌡 Example Usage * A [demo application](/ObjCStarter) showing the integration of our SDK in Objective-C language. diff --git a/docs/CustomCodeTemplates.md b/docs/CustomCodeTemplates.md new file mode 100644 index 00000000..7b632ba7 --- /dev/null +++ b/docs/CustomCodeTemplates.md @@ -0,0 +1,163 @@ +# Custom Code Templates + +iOS SDK 7.0.0 and above offers support for a custom presentation of in-app messages. This allows for utilizing the in-app notifications functionality with custom configuration and presentation logic. There are two types of Custom Code Templates that can be defined through the SDK: Templates and App Functions. Templates can contain action arguments while App Functions cannot. App Functions can be used as actions while Templates cannot. App Functions can be either 'visual' or not. 'Visual' functions can contain UI logic and will be part of the [In-App queue](#in-App-queue), while non-visual App Functions will be triggered directly when invoked and should not contain UI logic. + +## Creating templates and functions +All templates consist of a name, arguments and a presenter. They are all specified when creating a template through the builder. Name and presenter are required and names must be unique across the application. The template builders validate the correctness of the template definitions and will throw a `NSException` exception when an invalid template is being created. Template definitions must be valid in order to be triggered correctly. + +### Arguments +Arguments are key-value pairs that represent the configuration of the custom code templates. The supported argument types are: +- `BOOL`, `NSString`, `NSNumber`. They must have a default value which would be used if no other value is configured for the notification. +- Dictionary - A `Dictionary` of supported primitives with keys being the argument names. +- File - a file argument that will be downloaded when the template is triggered +- Action - an action argument that could be a function template or a built-in action like ‘close’ or ‘open url’ + +#### Hierarchical arguments +You can group arguments together by either using a dictionary argument or indicating the group in the argument's name by using a '.' symbol. Both definitions are treated the same. File and Action type arguments can only be added to a group by specifying it in the name of the argument. + +The following code snippets define identical arguments: +```swift +builder.addArgument("map", dictionary: [ + "a": 5, + "b": 6 +]) +``` +and +```swift +builder.addArgument("map.a", number: 5) +builder.addArgument("map.b", number: 6) +``` + +### Example +#### Objective-C +```objc +CTInAppTemplateBuilder *builder = [CTInAppTemplateBuilder new]; +[builder setName:@"template"]; +[builder setPresenter:presenter]; +[builder addArgument:@"string" withString:@"Default Text"]; +[builder addFileArgument:@"file"]; +[builder addArgument:@"int" withNumber:@0]; +CTCustomTemplate *template = [builder build]; +``` + +#### Swift +```swift +let templateBuilder = CTInAppTemplateBuilder() +templateBuilder.setName("template") +templateBuilder.setPresenter(presenter) +templateBuilder.addArgument("string", string: "Default Text") +templateBuilder.addFileArgument("file") +templateBuilder.addArgument("int", number: 0) +let template = templateBuilder.build() +``` + +## Registering custom templates +Templates must be registered before the CleverTap instance that would use them is created. A common place for this initialization is in `UIApplicationDelegate application:didFinishLaunchingWithOptions:`. If your application uses multiple `CleverTap` instances, use the `CleverTapInstanceConfig` to differentiate which templates should be registered to which `CleverTap` instance(s). + +Custom templates are registered through `CTCustomTemplatesManager.registerTemplateProducer` which accepts a `CTTemplateProducer` that contains the definitions of the templates. + +### Objective-C +```objc +#import +#import +#import + + +@interface TemplateProducer: NSObject + + +@end + + +@implementation TemplateProducer + + +- (NSSet * _Nonnull)defineTemplates:(CleverTapInstanceConfig * _Nonnull)instanceConfig { + CTInAppTemplateBuilder *builder = [CTInAppTemplateBuilder new]; + [builder setName:@"template"]; + [builder setPresenter:presenter]; + [builder addArgument:@"string" withString:@"Default Text"]; + [builder addFileArgument:@"file"]; + [builder addArgument:@"int" withNumber:@0]; + CTCustomTemplate *template = [builder build]; + + CTAppFunctionBuilder *functionBuilder = [[CTAppFunctionBuilder alloc] initWithIsVisual:YES]; + [functionBuilder setName:@"function"]; + [functionBuilder setPresenter:functionPresenter]; + [functionBuilder addArgument:@"int" withNumber:@0]; + CTCustomTemplate *function = [functionBuilder build]; + + return [[NSSet alloc] initWithObjects:template, function, nil]; +} + + +@end +``` + +### Swift +```swift +class TemplateProducer: CTTemplateProducer { + + + public func defineTemplates(_ instanceConfig: CleverTapInstanceConfig) -> Set { + let templateBuilder = CTInAppTemplateBuilder() + templateBuilder.setName("template") + templateBuilder.setPresenter(presenter) + templateBuilder.addArgument("string", string: "Default Text") + templateBuilder.addFileArgument("file") + templateBuilder.addArgument("int", number: 0) + let template = templateBuilder.build() + + let functionBuilder = CTAppFunctionBuilder(isVisual: true) + functionBuilder.setName("function") + functionBuilder.setPresenter(functionPresenter) + functionBuilder.addArgument("int", number: 0) + let function = functionBuilder.build() + + + return [template, function] + } +} +``` + +## Synching in-app templates to the dashboard + +In order for the templates to be usable in campaigns they must be synched with the dashboard. When all templates and functions are defined and registered in the SDK, they can be synched by: +```swift + cleverTapInstance.syncCustomTemplates() +``` +The synching can only be done in debug builds and with a SDK user that is marked as 'test user'. We recommend only running this function while developing the templates and delete the invocation in release builds. + +## Presenting templates + +When a custom template is triggered, its presenter will be invoked. Presenters must implement `CTTemplatePresenter`. Implement the `onPresent()` method in which to use the template invocation to present their custom UI logic. `CTTemplatePresenter` should also implement `onClose` which will be invoked when a template should be closed (which could occur when an action of type 'close' is triggered). Use this method to remove the UI associated with the template and call `context.dismissed`. + +All presenter methods provide a `CTTemplateContext` context. It can be used to: +- Obtain argument values by using the appropriate methods (`stringNamed:`, `numberNamed:` etc.). +- Trigger actions by their name through `triggerActionNamed:`. +- Set the state of the template invocation. `presented` and `dismissed` notify the SDK of the state of the current template invocation. The presented state is when an in-app is displayed to the user and the dismissed state is when the in-app is no longer being displayed. + +#### Template presenter +```swift +class Presenter: CTTemplatePresenter { + func onPresent(context: CTTemplateContext) { + // keep the context as long as the template UI is being displayed + // so that context.setDismissed() can be called when the UI is closed. + // showUI() + context.presented() + } + + func onCloseClicked(context: CTTemplateContext) { + // close the corresponding UI + context.dismissed() + } +} +``` + +Only one visual template or other InApp message can be displayed at a time by the SDK and no new messages can be shown until the current one is dismissed. + +### In-App queue +When an in-app needs to be shown it is added to a queue (depending on its priority) and is displayed when all messages before it have been dismissed. The queue is persisted to the storage and kept across app launches to ensure all messages are displayed when possible. The custom code in-apps behave in the same way. They will be triggered once their corresponding notification is the next in the queue to be shown. However since the control of the dismissal is left to the application's code, the next in-app message will not be shown until the current code template has called `context.dismissed()` + +### File downloading and caching +File arguments are automatically downloaded and are ready for use when an in-app template is presented. The files are downloaded when a file argument has changed and this file is not already cached. For client-side in-apps this happens both at App Launch and retried if needed when an in-app should be presented. For server-side in-apps the file downloading happens only before presenting the in-app. If any of the file arguments of an in-app fails to be downloaded, the whole in-app is skipped and the custom template will not be triggered. diff --git a/docs/Variables.md b/docs/Variables.md index 36b554bc..ede431c8 100644 --- a/docs/Variables.md +++ b/docs/Variables.md @@ -6,7 +6,7 @@ You can define variables using the CleverTap iOS SDK. When you define a variable Currently, CleverTap SDK supports the following variable types: - String -- boolean +- BOOL - Dictionary - int - float @@ -14,6 +14,7 @@ Currently, CleverTap SDK supports the following variable types: - short - long - Number +- File (supported from **v7.0.0**) # Define Variables @@ -79,7 +80,18 @@ let var_dict_nested = CleverTap.sharedInstance()?.defineVar(name: "var_dict_comp ``` +# Define File Variables +CleverTap supports file types for variables from `v7.0.0+`. Supported file types include but are not limited to images (jpg, jpeg, png, gif), text files, and PDFs. The File Variable is defined using the `defineFileVar` method, which returns an instance of a `CTVar` variable. The file variable does not have a default value. + +```objectivec +#import + +CTVar *var_file = [[CleverTap sharedInstance] defineFileVar:@"fileVariable"]; +``` +```swift Swift +let var_file = CleverTap.sharedInstance()?.defineFileVar(name: "fileVariable") +``` # Setup Callbacks @@ -90,6 +102,9 @@ CleverTap iOS SDK provides several callbacks for the developer to receive feedba - `onceVariablesChanged` - `onValueChanged` - Variables Delegate +- File variables Callback +- File variables individual Callback +- File variables Delegates ## Status of Variables Fetch Request @@ -112,8 +127,6 @@ CleverTap.sharedInstance()?.fetchVariables({ success in }]; ``` - - ## `onVariablesChanged` This callback is invoked when variables are initialized with values fetched from the server. It is called each time new values are fetched. @@ -139,8 +152,6 @@ CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withStri }]; ``` - - ## `onceVariablesChanged` This callback is invoked when variables are initialized with values fetched from the server. It is called only once. @@ -169,8 +180,6 @@ CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withStri ``` - - ## `onValueChanged` This callback is invoked when the value of the variable changes. @@ -195,8 +204,6 @@ CTVar *var_string = [[CleverTap sharedInstance] defineVar:@"var_string" withStri }]; ``` - - ## Variables Delegate The `VarDelegate` method is implemented to be invoked when the variable value is changed. @@ -231,7 +238,97 @@ CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; [var_string setDelegate:del]; ``` +## File Variables Callbacks + +### `onVariablesChangedAndNoDownloadsPending` + +This callback will be called when no files need to be downloaded or all downloads have been completed. It is called each time new values are fetched and downloads are completed. + +```objectivec +[[CleverTap sharedInstance] onVariablesChangedAndNoDownloadsPending:^{ + // Executed each time +}]; +``` +```swift +CleverTap.sharedInstance()?.onVariablesChangedAndNoDownloadsPending { + // Executed each time +} +``` + +### `onceVariablesChangedAndNoDownloadsPending` + +This callback will also be called when no files need to be downloaded or all downloads have been completed, but It is called only once. + +```objectivec +[[CleverTap sharedInstance] onceVariablesChangedAndNoDownloadsPending:^{ + // Executed only once +}]; +``` +```swift +CleverTap.sharedInstance()?.onceVariablesChangedAndNoDownloadsPending { + // Executed only once +} +``` +## File variables individual Callback + +### `onFileIsReady` + +This callback will be called when the value of the file variable is downloaded and ready. This is only available for File variables. + +```objectivec +#import + +CTVar *var_file = [[CleverTap sharedInstance] defineFileVar:@"fileVariable"]; +[var_file onFileIsReady:^{ + // Called when file is downloaded. +}]; +``` +```swift +let var_file = CleverTap.sharedInstance()?.defineFileVar(name: "fileVariable") + +var_file?.onFileIsReady { + // Called when file is downloaded. +} +``` + +## File Variables Delegates + +The `fileIsReady` method is called when file is downloaded. This method is only for file type variables and variable's value will return the file downloaded path. + +```objectivec +#import + +@interface CTVarDelegateImpl : NSObject +@end + + +@implementation CTVarDelegateImpl +- (void)valueDidChange:(CTVar *)variable { +// valueDidChange +} + +- (void)fileIsReady:(CTVar *)var { + NSLog(@"CleverTap file var:%@ is downloaded at path: %@", var.name ,var.value); +} +@end + +CTVarDelegateImpl *del = [[CTVarDelegateImpl alloc] init]; +[var_file setDelegate:del]; +``` +```swift +@objc class VarDelegateImpl: NSObject, VarDelegate { + func valueDidChange(_ variable: CleverTapSDK.Var) { + print("CleverTap \(String(describing: variable.name)):valueDidChange to: \(variable.value!)") + } + + func fileIsReady(_ variable: CleverTapSDK.Var) { + print("CleverTap file downloaded to path: \(variable.value ?? "nil")") + } +} + +var_file?.setDelegate(self) +``` # Sync Defined Variables @@ -262,8 +359,6 @@ CleverTap.sharedInstance()?.syncVariables(); [[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. @@ -290,8 +385,6 @@ CleverTap.sharedInstance()?.fetchVariables({ success in }]; ``` - - # Use Fetched Variables Values This process involves the following two major steps: @@ -318,15 +411,13 @@ variable?.onValueChanged { }]; ``` - - ## 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: +You can use several methods on the `Var` instance as shown in the following code. For File Variables, value returns the file downloaded path. ```swift // Swift @@ -335,6 +426,12 @@ variable?.defaultValue // returns default value variable?.value // returns current value variable?.numberValue // returns value as NSNumber if applicable variable?.stringValue // returns value as String + +// File Variables +let var_file = CleverTap.sharedInstance()?.defineFileVar(name: "fileVariable") +var_file?.value // returns file downloaded path +var_file?.stringValue // returns file downloaded path +var_file?.fileValue // returns file downloaded path ``` ```objectivec // Objective-C @@ -343,9 +440,13 @@ variable.defaultValue; // returns default value variable.value; // returns current value variable.numberValue; // returns value as NSNumber if applicable variable.stringValue; // returns value as String -``` - +// File Variables +CTVar *var_file = [[CleverTap sharedInstance] defineFileVar:@"fileVariable"]; +var_file.value // returns file downloaded path +var_file.stringValue // returns file downloaded path +var_file.fileValue // returns file downloaded path +``` ### Using `CleverTap` Instance method diff --git a/sdk-version.txt b/sdk-version.txt index 0df17dd0..41225218 100644 --- a/sdk-version.txt +++ b/sdk-version.txt @@ -1 +1 @@ -6.2.1 \ No newline at end of file +7.0.0 \ No newline at end of file