From 6ac4230b9d21e1b837316a387ce1621906e58a5e Mon Sep 17 00:00:00 2001 From: Arnaud Roland Date: Fri, 26 Apr 2024 18:41:04 +0200 Subject: [PATCH] release: SDK 2.0.0 --- Package.swift | 4 +- Sources/Batch.xcodeproj/project.pbxproj | 362 +++++++++---- .../xcshareddata/xcschemes/Batch.xcscheme | 2 +- .../xcschemes/BatchStatic.xcscheme | 2 +- .../xcschemes/BatchTests.xcscheme | 2 +- .../xcschemes/BatchUniversal.xcscheme | 2 +- .../xcschemes/BatchXCFramework.xcscheme | 2 +- Sources/Batch/BAErrorHelper.h | 1 - Sources/Batch/Batch.h | 5 +- Sources/Batch/Batch.modulemap | 10 +- Sources/Batch/BatchActions.h | 5 +- Sources/Batch/BatchCore.h | 129 ++--- Sources/Batch/BatchCore.m | 42 +- Sources/Batch/BatchDataCollectionConfig.h | 39 ++ Sources/Batch/BatchDataCollectionConfig.m | 39 ++ ...atchEventData.h => BatchEventAttributes.h} | 48 +- Sources/Batch/BatchEventAttributes.m | 231 +++++++++ ...rivate.h => BatchEventAttributesPrivate.h} | 23 +- Sources/Batch/BatchEventData.m | 334 ------------ Sources/Batch/BatchInbox.h | 22 - Sources/Batch/BatchInbox.m | 11 - Sources/Batch/BatchInboxPrivate.h | 2 - Sources/Batch/BatchLogger.h | 4 +- Sources/Batch/BatchMessaging.h | 16 +- Sources/Batch/BatchMessaging.m | 12 + Sources/Batch/BatchProfile.h | 148 ++++++ Sources/Batch/BatchProfile.m | 54 ++ Sources/Batch/BatchProfileEditor.h | 281 ++++++++++ Sources/Batch/BatchProfileEditor.m | 217 ++++++++ Sources/Batch/BatchPush.h | 229 +++++---- Sources/Batch/BatchPush.m | 80 +-- Sources/Batch/BatchPushPrivate.h | 45 -- Sources/Batch/BatchUser.h | 403 +-------------- Sources/Batch/BatchUser.m | 216 +------- Sources/Batch/BatchUserProfile.h | 34 -- Sources/Batch/BatchUserProfile.m | 77 --- Sources/Batch/Defined.h | 30 +- .../Kernel/Dependency Injection/BAInjection.h | 4 +- .../Dependency Injection/BAInjection.swift | 29 ++ .../Private/BAInjectionRegistrar.m | 22 +- .../Foundation/BATRegularExpression.swift | 44 ++ .../Batch/Kernel/Foundation/BATSDKError.swift | 49 ++ .../Helpers/BADelegatedApplicationDelegate.m | 113 ----- Sources/Batch/Kernel/Helpers/BAEmailUtils.h | 24 - Sources/Batch/Kernel/Helpers/BAEmailUtils.m | 28 - .../Helpers/BAPartialApplicationDelegate.h | 25 - Sources/Batch/Kernel/Helpers/BAWindowHelper.h | 4 +- Sources/Batch/Kernel/Helpers/BAWindowHelper.m | 26 +- Sources/Batch/Kernel/Logger/BALogger.m | 2 +- .../Kernel/Parameters/BANetworkParameters.h | 15 - .../Kernel/Parameters/BANetworkParameters.m | 84 --- .../Kernel/Parameters/BAPropertiesCenter.m | 45 +- .../Batch/Kernel/Parameters/BAUserDefaults.h | 16 - .../Batch/Kernel/Parameters/BAUserDefaults.m | 15 - .../Batch/Modules/Actions/BAActionsCenter.m | 9 +- .../Actions/BAUserDataBuiltinActions.m | 15 +- .../Actions/BAUserEventBuiltinActions.m | 31 +- .../Modules/Core/BAApplicationLifecycle.m | 40 +- .../Modules/Core/BACenterMulticastDelegate.h | 28 - .../Modules/Core/BACenterMulticastDelegate.m | 39 +- Sources/Batch/Modules/Core/BAConfiguration.h | 33 +- Sources/Batch/Modules/Core/BAConfiguration.m | 34 +- Sources/Batch/Modules/Core/BACoreCenter.h | 35 -- Sources/Batch/Modules/Core/BACoreCenter.m | 206 ++++---- .../Core/BANotificationAuthorization.h | 8 - .../Core/BANotificationAuthorization.m | 22 - .../Modules/Core/BATInternalEvents.swift | 13 + Sources/Batch/Modules/Core/BAUserProfile.h | 15 - Sources/Batch/Modules/Core/BAUserProfile.m | 45 -- .../BATDataCollectionCenter.swift | 140 +++++ .../Data Collection/BATSystemParameter.swift | 199 ++++++++ .../Data Collection/DataCollectionUtils.swift | 31 ++ Sources/Batch/Modules/Inbox/BAInbox.m | 5 +- .../Local Campaigns/BALocalCampaignsCenter.h | 4 +- .../Local Campaigns/BALocalCampaignsCenter.m | 17 +- .../Local Campaigns/BALocalCampaignsManager.m | 1 + .../Signals/BAPublicEventTrackedSignal.h | 6 +- .../Signals/BAPublicEventTrackedSignal.m | 6 +- .../Modules/Messaging/BAMSGImageDownloader.m | 6 +- .../Modules/Messaging/BAMSGPayloadParser.m | 1 + .../Modules/Messaging/BAMessagingCenter.h | 4 +- .../Modules/Messaging/BAMessagingCenter.m | 69 ++- .../Messaging/FeedbackGenerators.swift | 87 ++++ .../UI/BAMSGBaseBannerViewController.m | 25 +- .../Messaging/UI/BAMSGImageViewController.m | 11 +- .../UI/BAMSGInterstitialViewController.m | 13 +- .../Modules/Messaging/UI/BAMSGVideoView.m | 4 + .../Messaging/UI/BAMSGViewController.m | 23 +- .../Messaging/UI/BAMSGWebviewViewController.m | 17 +- .../Webview/BATWebviewJavascriptBridge.h | 3 - .../Webview/BATWebviewJavascriptBridge.m | 15 +- .../Widgets/BAMSGActivityIndicatorView.m | 28 +- .../Widgets/BAMSGPannableAlertContainerView.m | 5 +- .../BAMSGPannableAnchoredContainerView.m | 11 +- .../Messaging/Widgets/BAMSGRemoteImageView.m | 3 +- .../Modules/Profile/BAProfileCenter.swift | 219 ++++++++ .../BATEventAttributesSerializer.swift | 80 +++ .../Profile/BATEventAttributesValidator.swift | 348 +++++++++++++ .../Profile/BATProfileDataValidators.swift | 52 ++ .../Modules/Profile/BATProfileEditor.swift | 480 ++++++++++++++++++ .../BATProfileInstallDataCompatibility.swift | 78 +++ .../BATProfileOperationsSerializer.swift | 84 +++ .../Profile/BatchProfileError+Init.swift | 15 + Sources/Batch/Modules/Push/BAPushCenter.h | 61 +-- Sources/Batch/Modules/Push/BAPushCenter.m | 161 +----- .../Batch/Modules/Push/BAPushSystemHelper.m | 187 +------ .../Modules/Push/BAPushSystemHelperProtocol.h | 10 +- Sources/Batch/Modules/Tracker/BAEvent.m | 5 + .../Modules/Tracker/BAEventSQLiteDatasource.m | 9 +- .../Tracker/BATEventTrackerProtocol.swift | 31 ++ .../Batch/Modules/Tracker/BATrackerCenter.h | 45 -- .../Batch/Modules/Tracker/BATrackerCenter.m | 231 ++++----- .../Batch/Modules/Tracker/BATrackerSender.m | 5 - ...UserDataEditor.h => BAInstallDataEditor.h} | 26 +- ...UserDataEditor.m => BAInstallDataEditor.m} | 247 ++------- .../Batch/Modules/User/BAUserDataManager.h | 18 +- .../Batch/Modules/User/BAUserDataManager.m | 53 +- .../Modules/User/BAUserEmailSubscription.h | 29 -- .../Modules/User/BAUserEmailSubscription.m | 108 ---- Sources/Batch/PrivateUmbrellaHeader.h | 10 +- Sources/Batch/Versions.h | 4 +- .../Batch/Webservices/BAWebserviceMetrics.h | 68 --- .../Batch/Webservices/BAWebserviceMetrics.m | 109 ---- Sources/Batch/Webservices/Core/BAConnection.h | 2 +- Sources/Batch/Webservices/Core/BAURLSession.m | 6 +- .../Core/BAWebserviceMsgPackClient.m | 1 + .../BALocalCampaignsJITService.m | 5 - .../BAUserDataServices.m | 24 + .../Response Models/BAWSResponseAttributes.h | 6 + .../Response Models/BAWSResponseAttributes.m | 4 + .../BAWSResponseAttributesCheck.h | 6 + .../BAWSResponseAttributesCheck.m | 4 + .../Query/BAQueryWebserviceClient.m | 30 -- .../Query/BAQueryWebserviceClientDelegate.h | 2 + ...andardQueryWebserviceIdentifiersProvider.m | 42 +- Sources/batchTests/Kernel/GZip/gzipTests.m | 2 +- .../Kernel/Helpers/emailTests.swift | 28 - .../Kernel/Helpers/swizzlingTests.m | 74 +-- .../Kernel/Logger/internalLoggerTests.swift | 4 +- .../Mocks/BatchUserDataEditorMock.swift | 36 ++ .../{Stubs => Mocks}/DeeplinkDelegateStub.h | 0 .../{Stubs => Mocks}/DeeplinkDelegateStub.m | 0 .../batchTests/Mocks/EventTrackerMock.swift | 72 +++ .../Modules/Actions/builtinActionsTest.m | 152 +++--- .../Modules/Core/batchConfigurationTests.m | 51 -- .../Modules/Core/batchCoreCenterTests.m | 5 + .../Modules/Core/batchOptOutTests.m | 18 +- .../Modules/Core/batchUserProfileTests.m | 244 --------- .../dataCollectionCenterTests.swift | 119 +++++ .../Modules/Inbox/inboxFetcherTests.swift | 2 +- .../Inbox/inboxNotificationContentTests.m | 14 +- .../webviewJavascriptBridgeTests.swift | 15 +- .../Webview/webviewTestHelpers.swift | 2 +- .../Modules/Profile/TestProfileCenter.swift | 30 ++ .../Modules/Profile/TestProfileEditor.swift | 18 + .../Profile/eventDataSerializerTests.swift | 92 ++++ .../Profile/eventDataValidatorTests.swift | 212 ++++++++ .../Profile/profileEditorCompatibilityTest.m | 72 +++ .../profileEditorValidationTests.swift | 49 ++ .../Profile/profileIdentifyTests.swift | 97 ++++ .../Profile/profileMigrationTests.swift | 139 +++++ .../profileOperationsSerializerTests.swift | 136 +++++ ...tchUNUserNotificationCenterDelegateTests.m | 23 +- .../Modules/Push/pushAuthorizationTests.swift | 60 +++ .../Tracker/batchEventSQLiteDatasourceTests.m | 30 +- .../User/batchUserEditorPublicAPITests.m | 93 ---- .../User/batchUserEmailSubscriptionTests.m | 92 ---- .../batchTests/Modules/User/batchUserTests.m | 57 +-- .../Modules/User/userDataManagerTests.swift | 24 + .../Modules/batchUserDataEditorTests.swift | 131 +---- .../Modules/mockUserDatasource.swift | 110 ++++ Sources/batchTests/XCTest+Injection.swift | 31 ++ Sources/batchTests/XCTest+Threading.swift | 42 ++ Sources/batchTests/batchCoreTests.m | 21 +- .../batchEventAttributesTests.swift | 60 +++ Sources/batchTests/batchEventDataTests.swift | 183 ------- Sources/batchTests/deeplinkDelegateTests.m | 35 +- Sources/bridgy.config.json | 2 +- Tools/Dockerfile.format | 10 +- Tools/Package.resolved | 4 +- Tools/Package.swift | 4 +- Tools/Scripts/format-check.sh | 6 +- Tools/Scripts/format.sh | 6 +- .../TemplateIcon-1016.png | Bin 0 -> 2334 bytes .../TemplateIcon-1016@2x.png | Bin 0 -> 5693 bytes .../TemplateInfo.plist | 87 ++++ .../XCTestCaseSwift/___FILEBASENAME___.swift | 21 + 187 files changed, 5960 insertions(+), 4775 deletions(-) create mode 100644 Sources/Batch/BatchDataCollectionConfig.h create mode 100644 Sources/Batch/BatchDataCollectionConfig.m rename Sources/Batch/{BatchEventData.h => BatchEventAttributes.h} (53%) create mode 100644 Sources/Batch/BatchEventAttributes.m rename Sources/Batch/{BatchEventDataPrivate.h => BatchEventAttributesPrivate.h} (62%) delete mode 100644 Sources/Batch/BatchEventData.m create mode 100644 Sources/Batch/BatchProfile.h create mode 100644 Sources/Batch/BatchProfile.m create mode 100644 Sources/Batch/BatchProfileEditor.h create mode 100644 Sources/Batch/BatchProfileEditor.m delete mode 100644 Sources/Batch/BatchPushPrivate.h delete mode 100644 Sources/Batch/BatchUserProfile.h delete mode 100644 Sources/Batch/BatchUserProfile.m create mode 100644 Sources/Batch/Kernel/Dependency Injection/BAInjection.swift create mode 100644 Sources/Batch/Kernel/Foundation/BATRegularExpression.swift create mode 100644 Sources/Batch/Kernel/Foundation/BATSDKError.swift delete mode 100644 Sources/Batch/Kernel/Helpers/BAEmailUtils.h delete mode 100644 Sources/Batch/Kernel/Helpers/BAEmailUtils.m delete mode 100644 Sources/Batch/Kernel/Parameters/BANetworkParameters.h delete mode 100644 Sources/Batch/Kernel/Parameters/BANetworkParameters.m create mode 100644 Sources/Batch/Modules/Core/BATInternalEvents.swift create mode 100644 Sources/Batch/Modules/Data Collection/BATDataCollectionCenter.swift create mode 100644 Sources/Batch/Modules/Data Collection/BATSystemParameter.swift create mode 100644 Sources/Batch/Modules/Data Collection/DataCollectionUtils.swift create mode 100644 Sources/Batch/Modules/Messaging/FeedbackGenerators.swift create mode 100644 Sources/Batch/Modules/Profile/BAProfileCenter.swift create mode 100644 Sources/Batch/Modules/Profile/BATEventAttributesSerializer.swift create mode 100644 Sources/Batch/Modules/Profile/BATEventAttributesValidator.swift create mode 100644 Sources/Batch/Modules/Profile/BATProfileDataValidators.swift create mode 100644 Sources/Batch/Modules/Profile/BATProfileEditor.swift create mode 100644 Sources/Batch/Modules/Profile/BATProfileInstallDataCompatibility.swift create mode 100644 Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift create mode 100644 Sources/Batch/Modules/Profile/BatchProfileError+Init.swift create mode 100644 Sources/Batch/Modules/Tracker/BATEventTrackerProtocol.swift rename Sources/Batch/Modules/User/{BAUserDataEditor.h => BAInstallDataEditor.h} (80%) rename Sources/Batch/Modules/User/{BAUserDataEditor.m => BAInstallDataEditor.m} (74%) delete mode 100644 Sources/Batch/Modules/User/BAUserEmailSubscription.h delete mode 100644 Sources/Batch/Modules/User/BAUserEmailSubscription.m delete mode 100644 Sources/Batch/Webservices/BAWebserviceMetrics.h delete mode 100644 Sources/Batch/Webservices/BAWebserviceMetrics.m delete mode 100644 Sources/batchTests/Kernel/Helpers/emailTests.swift create mode 100644 Sources/batchTests/Mocks/BatchUserDataEditorMock.swift rename Sources/batchTests/{Stubs => Mocks}/DeeplinkDelegateStub.h (100%) rename Sources/batchTests/{Stubs => Mocks}/DeeplinkDelegateStub.m (100%) create mode 100644 Sources/batchTests/Mocks/EventTrackerMock.swift delete mode 100644 Sources/batchTests/Modules/Core/batchUserProfileTests.m create mode 100644 Sources/batchTests/Modules/Data Collection/dataCollectionCenterTests.swift create mode 100644 Sources/batchTests/Modules/Profile/TestProfileCenter.swift create mode 100644 Sources/batchTests/Modules/Profile/TestProfileEditor.swift create mode 100644 Sources/batchTests/Modules/Profile/eventDataSerializerTests.swift create mode 100644 Sources/batchTests/Modules/Profile/eventDataValidatorTests.swift create mode 100644 Sources/batchTests/Modules/Profile/profileEditorCompatibilityTest.m create mode 100644 Sources/batchTests/Modules/Profile/profileEditorValidationTests.swift create mode 100644 Sources/batchTests/Modules/Profile/profileIdentifyTests.swift create mode 100644 Sources/batchTests/Modules/Profile/profileMigrationTests.swift create mode 100644 Sources/batchTests/Modules/Profile/profileOperationsSerializerTests.swift create mode 100644 Sources/batchTests/Modules/Push/pushAuthorizationTests.swift delete mode 100644 Sources/batchTests/Modules/User/batchUserEditorPublicAPITests.m delete mode 100644 Sources/batchTests/Modules/User/batchUserEmailSubscriptionTests.m create mode 100644 Sources/batchTests/Modules/User/userDataManagerTests.swift create mode 100644 Sources/batchTests/Modules/mockUserDatasource.swift create mode 100644 Sources/batchTests/XCTest+Injection.swift create mode 100644 Sources/batchTests/XCTest+Threading.swift create mode 100644 Sources/batchTests/batchEventAttributesTests.swift delete mode 100644 Sources/batchTests/batchEventDataTests.swift create mode 100644 Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016.png create mode 100644 Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016@2x.png create mode 100644 Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateInfo.plist create mode 100644 Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/XCTestCaseSwift/___FILEBASENAME___.swift diff --git a/Package.swift b/Package.swift index 93a1be0..bba0712 100644 --- a/Package.swift +++ b/Package.swift @@ -18,8 +18,8 @@ let package = Package( targets: [ .binaryTarget( name: "Batch", - url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-1.21.2.zip", - checksum: "5caa61a570d8317f4f5a75e4325c2bfcbc5f9b98349bffd5c2fc21375755da25" + url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-2.0.0.zip", + checksum: "9e78ad2e7320d91a9b1b3ca8f2069f260cf96d6d93309e9d5504f2b551ecbda2" ) ] ) \ No newline at end of file diff --git a/Sources/Batch.xcodeproj/project.pbxproj b/Sources/Batch.xcodeproj/project.pbxproj index 42b787a..2f6a775 100644 --- a/Sources/Batch.xcodeproj/project.pbxproj +++ b/Sources/Batch.xcodeproj/project.pbxproj @@ -43,15 +43,11 @@ 02849C1C19B9FA2D0013408B /* batchTrackerCenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849BE419B9FA2D0013408B /* batchTrackerCenterTests.m */; }; 02849C2C19B9FA2D0013408B /* batchWebserviceResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849BF719B9FA2D0013408B /* batchWebserviceResponseTests.m */; }; 02849C2E19B9FA2D0013408B /* batchResponseHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849BFA19B9FA2D0013408B /* batchResponseHelperTests.m */; }; - 02955F5E19DAE0E800E7458A /* batchUserProfileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 02955F5D19DAE0E800E7458A /* batchUserProfileTests.m */; }; 029B335A1A39A1F7006B9DC6 /* batchNSURLREquest+BAHeadersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 029B33481A39A1F7006B9DC6 /* batchNSURLREquest+BAHeadersTests.m */; }; 029B335B1A39A1F7006B9DC6 /* batchAESTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 029B334A1A39A1F7006B9DC6 /* batchAESTests.m */; }; 029B335E1A39A1F7006B9DC6 /* batchParametersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 029B334E1A39A1F7006B9DC6 /* batchParametersTests.m */; }; 029B335F1A39A1F7006B9DC6 /* batchPropertiesCenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 029B334F1A39A1F7006B9DC6 /* batchPropertiesCenterTests.m */; }; 029B33601A39A1F7006B9DC6 /* batchUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 029B33501A39A1F7006B9DC6 /* batchUserDefaultsTests.m */; }; - 1E104EAF28E6F43D00E998AE /* BAUserEmailSubscription.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E104EAD28E6F43D00E998AE /* BAUserEmailSubscription.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 1E104EB028E6F43D00E998AE /* BAUserEmailSubscription.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E104EAE28E6F43D00E998AE /* BAUserEmailSubscription.m */; }; - 1E104EB628EB289400E998AE /* batchUserEmailSubscriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E104EB528EB289400E998AE /* batchUserEmailSubscriptionTests.m */; }; 1E2C31B527CF740E00A4CB6E /* BAMetricRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E2C31B327CF740E00A4CB6E /* BAMetricRegistry.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1E2C31B627CF740E00A4CB6E /* BAMetricRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E2C31B427CF740E00A4CB6E /* BAMetricRegistry.m */; }; 1E45578527CE1B510048D3A8 /* BALocalCampaignsJITService.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E45578327CE1B510048D3A8 /* BALocalCampaignsJITService.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -65,9 +61,7 @@ 1EA42DFA26A85D7D004CC618 /* BADBGFindMyInstallationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EA42DF726A84C71004CC618 /* BADBGFindMyInstallationHelper.m */; }; 1EA42DFB26A85DF9004CC618 /* BADBGFindMyInstallationHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EA42DF926A84C99004CC618 /* BADBGFindMyInstallationHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1EA42E0426A9A736004CC618 /* FindMyInstallationHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EA42E0226A9A716004CC618 /* FindMyInstallationHelperTests.m */; }; - 1EB69D3B290C09B9003E1EF7 /* BAEmailUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EB69D39290C09B9003E1EF7 /* BAEmailUtils.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 1EB69D3C290C09B9003E1EF7 /* BAEmailUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EB69D3A290C09B9003E1EF7 /* BAEmailUtils.m */; }; - 1EB69D3E290C0F5A003E1EF7 /* emailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB69D3D290C0F5A003E1EF7 /* emailTests.swift */; }; + 1EB69D3E290C0F5A003E1EF7 /* profileEditorValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB69D3D290C0F5A003E1EF7 /* profileEditorValidationTests.swift */; }; 1EBDBC5F27146B8B006DC662 /* BAMetric.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF8BA98271462D90063E117 /* BAMetric.m */; }; 1EBDBC6027146C97006DC662 /* BAMetric.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF8BA9A271462F80063E117 /* BAMetric.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1EBDBC6327157A3A006DC662 /* BACounter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EBDBC6127157A3A006DC662 /* BACounter.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -86,6 +80,7 @@ 1EC9657227AD7879007D66AD /* BALocalCampaignsGlobalCappings.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC9657027AD7879007D66AD /* BALocalCampaignsGlobalCappings.m */; }; 1EF1974427673BBE00386DF0 /* BAInjectionRegistrar.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF1974227673BBE00386DF0 /* BAInjectionRegistrar.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1EF1974527673BBE00386DF0 /* BAInjectionRegistrar.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF1974327673BBE00386DF0 /* BAInjectionRegistrar.m */; }; + 510737422BC596950033E2E8 /* profileEditorCompatibilityTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 510737412BC596950033E2E8 /* profileEditorCompatibilityTest.m */; }; 510BD5C522E0B4BA00D8CBA1 /* BAWSResponseAttributesCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = 510BD5BD22E0B4BA00D8CBA1 /* BAWSResponseAttributesCheck.m */; }; 510BD5C622E0B4BA00D8CBA1 /* BAWSResponseAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 510BD5BE22E0B4BA00D8CBA1 /* BAWSResponseAttributes.m */; }; 510BD5C722E0B4BA00D8CBA1 /* BAWSResponseAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 510BD5BF22E0B4BA00D8CBA1 /* BAWSResponseAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -103,6 +98,10 @@ 510BD5DB22E0B4D500D8CBA1 /* BAWSQueryStart.h in Headers */ = {isa = PBXBuildFile; fileRef = 510BD5D322E0B4D500D8CBA1 /* BAWSQueryStart.h */; settings = {ATTRIBUTES = (Private, ); }; }; 510BD5DC22E0B4D500D8CBA1 /* BAWSQueryStart.m in Sources */ = {isa = PBXBuildFile; fileRef = 510BD5D422E0B4D500D8CBA1 /* BAWSQueryStart.m */; }; 510D906F24C83A81005981AC /* shaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510D906E24C83A81005981AC /* shaTests.swift */; }; + 510F99262B8E2F35009D15DE /* BATSDKError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510F99252B8E2F35009D15DE /* BATSDKError.swift */; }; + 510F99282B8E36EE009D15DE /* BATRegularExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510F99272B8E36EE009D15DE /* BATRegularExpression.swift */; }; + 510F992A2B8E9192009D15DE /* BATEventAttributesValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510F99292B8E9192009D15DE /* BATEventAttributesValidator.swift */; }; + 510F99362B8F8544009D15DE /* BATEventAttributesSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510F99352B8F8544009D15DE /* BATEventAttributesSerializer.swift */; }; 510FFEE822FD800700BBF277 /* dependencyInjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510FFEE622FD7FE500BBF277 /* dependencyInjectionTests.swift */; }; 51103A792A7405B50082987C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 51103A782A7405B40082987C /* PrivacyInfo.xcprivacy */; }; 511B91FE22CE32A3002BC4A8 /* Batch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 517D701822BBC34A00E374FB /* Batch.framework */; }; @@ -128,6 +127,7 @@ 5145C78E24C587EA004A89FE /* BASHA.h in Headers */ = {isa = PBXBuildFile; fileRef = 5145C78C24C587EA004A89FE /* BASHA.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5145C78F24C587EA004A89FE /* BASHA.m in Sources */ = {isa = PBXBuildFile; fileRef = 5145C78D24C587EA004A89FE /* BASHA.m */; }; 51496A2F25110D930051B8A4 /* BAPartialApplicationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 51496A2E25110D680051B8A4 /* BAPartialApplicationDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 514B39A82BAA095D006980EB /* eventDataSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B39A72BAA095D006980EB /* eventDataSerializerTests.swift */; }; 514BA4A027D257F100B93FAF /* inboxFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BA49F27D257F100B93FAF /* inboxFetcherTests.swift */; }; 514CD72B1BFE2A2C00F79110 /* batchUserDataSQLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 514CD72A1BFE2A2B00F79110 /* batchUserDataSQLTests.m */; }; 514FE72A2209F5C0003C3426 /* batchOptOutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 514FE7292209F5C0003C3426 /* batchOptOutTests.m */; }; @@ -231,6 +231,7 @@ 516D7173260924E500CF7F5B /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 5154644125121BC40008E201 /* OCProtocolMockObject.m */; }; 516D7174260924E500CF7F5B /* OCMock.h in Sources */ = {isa = PBXBuildFile; fileRef = 515463E925121B960008E201 /* OCMock.h */; }; 516D717A260924F000CF7F5B /* libOCMock-Batch.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 516D7117260924CC00CF7F5B /* libOCMock-Batch.a */; }; + 516D98AB2BC05A0C00345778 /* BATProfileInstallDataCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516D98AA2BC05A0C00345778 /* BATProfileInstallDataCompatibility.swift */; }; 51711AC023D60A5300FF2B9D /* PrivateUmbrellaHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 51711ABF23D60A5300FF2B9D /* PrivateUmbrellaHeader.h */; settings = {ATTRIBUTES = (Private, ); }; }; 51711AC123D60CBA00FF2B9D /* BAEventDispatcherCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 625870312354CA01006B0287 /* BAEventDispatcherCenter.h */; settings = {ATTRIBUTES = (Private, ); }; }; 51711AC223D60CBB00FF2B9D /* BAQueryWebserviceClientDatasource.h in Headers */ = {isa = PBXBuildFile; fileRef = 51697DCD22DC847D0043FE49 /* BAQueryWebserviceClientDatasource.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -238,6 +239,9 @@ 51711AC423D60CBB00FF2B9D /* BAOverlayedInjectable-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 51379B8222FC7C3100AB7B82 /* BAOverlayedInjectable-Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5171445B22DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 5171445922DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5171445C22DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 5171445A22DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.m */; }; + 5172DD0C2B88E2620094784D /* BAProfileCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5172DD0B2B88E2620094784D /* BAProfileCenter.swift */; }; + 5172DD112B88EE230094784D /* BATInternalEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5172DD102B88EE230094784D /* BATInternalEvents.swift */; }; + 5172DD132B88F7B80094784D /* BATProfileDataValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5172DD122B88F7B80094784D /* BATProfileDataValidators.swift */; }; 5179E71E22FC15C000614F9D /* BAInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 5179E71C22FC15C000614F9D /* BAInjection.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5179E71F22FC15C000614F9D /* BAInjection.m in Sources */ = {isa = PBXBuildFile; fileRef = 5179E71D22FC15C000614F9D /* BAInjection.m */; }; 5179E72222FC2BC900614F9D /* BAInjectable.h in Headers */ = {isa = PBXBuildFile; fileRef = 5179E72022FC2BC900614F9D /* BAInjectable.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -250,8 +254,7 @@ 517CC29325B6E02A00A3F380 /* BATWebviewBridgeWKHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 517CC29125B6E02A00A3F380 /* BATWebviewBridgeWKHandler.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517CC29425B6E02A00A3F380 /* BATWebviewBridgeWKHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 517CC29225B6E02A00A3F380 /* BATWebviewBridgeWKHandler.m */; }; 517D702022BBC45900E374FB /* BatchCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0284998219B9EFBA0013408B /* BatchCore.m */; }; - 517D702122BBC45900E374FB /* BatchEventData.m in Sources */ = {isa = PBXBuildFile; fileRef = 511F1E1621109D3800796AD9 /* BatchEventData.m */; }; - 517D702222BBC45900E374FB /* BatchUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 022AC42B19DAA36900BDC3E7 /* BatchUserProfile.m */; }; + 517D702122BBC45900E374FB /* BatchEventAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 511F1E1621109D3800796AD9 /* BatchEventAttributes.m */; }; 517D702322BBC45900E374FB /* BatchInbox.m in Sources */ = {isa = PBXBuildFile; fileRef = 518E38421EA8EB93009AB274 /* BatchInbox.m */; }; 517D702422BBC45900E374FB /* BatchPush.m in Sources */ = {isa = PBXBuildFile; fileRef = 0284998719B9EFBA0013408B /* BatchPush.m */; }; 517D702522BBC45900E374FB /* BatchUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 5101539E1BEA5318009E8B16 /* BatchUser.m */; }; @@ -334,7 +337,7 @@ 517D708322BBC45900E374FB /* BAUserSQLiteDatasource.m in Sources */ = {isa = PBXBuildFile; fileRef = 510153A71BED11C4009E8B16 /* BAUserSQLiteDatasource.m */; }; 517D708422BBC45900E374FB /* BAUserDataOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 512354B61BFA2472001C5259 /* BAUserDataOperation.m */; }; 517D708522BBC45900E374FB /* BAUserDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5110746A1C208F6300BFC215 /* BAUserDataManager.m */; }; - 517D708622BBC45900E374FB /* BAUserDataEditor.m in Sources */ = {isa = PBXBuildFile; fileRef = 512354BC1BFA30EB001C5259 /* BAUserDataEditor.m */; }; + 517D708622BBC45900E374FB /* BAInstallDataEditor.m in Sources */ = {isa = PBXBuildFile; fileRef = 512354BC1BFA30EB001C5259 /* BAInstallDataEditor.m */; }; 517D708722BBC45900E374FB /* BAUserAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = 517EAE421C1890540058D288 /* BAUserAttribute.m */; }; 517D708822BBC45900E374FB /* BAUserDataServices.m in Sources */ = {isa = PBXBuildFile; fileRef = 511074641C208B7800BFC215 /* BAUserDataServices.m */; }; 517D708922BBC45900E374FB /* BAActionsCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 514671C21D647AFE00C50113 /* BAActionsCenter.m */; }; @@ -362,7 +365,6 @@ 517D70B122BBC45900E374FB /* BAResponseHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849A4919B9EFBA0013408B /* BAResponseHelper.m */; }; 517D70B222BBC45900E374FB /* BAGETWebserviceClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 518E384E1EAA5EF7009AB274 /* BAGETWebserviceClient.m */; }; 517D70B322BBC45900E374FB /* BAQueryWebserviceClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849A4B19B9EFBA0013408B /* BAQueryWebserviceClient.m */; }; - 517D70B522BBC45900E374FB /* BAWebserviceMetrics.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C7D24F1B1359700038FAD7 /* BAWebserviceMetrics.m */; }; 517D70B622BBC45900E374FB /* BALogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 5124663C1DAD125800B14F1E /* BALogger.m */; }; 517D70B822BBC45900E374FB /* BALoggerUnified.m in Sources */ = {isa = PBXBuildFile; fileRef = 512466461DAD1C2700B14F1E /* BALoggerUnified.m */; }; 517D70B922BBC45900E374FB /* BANullHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 516186F71C84B0C9009304A6 /* BANullHelper.m */; }; @@ -385,7 +387,6 @@ 517D70CC22BBC45900E374FB /* BASystemDateProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 5155F79F1EF95E5B00929DD8 /* BASystemDateProvider.m */; }; 517D70CD22BBC45900E374FB /* BATZAwareDate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5113B65D1F27497400CDB623 /* BATZAwareDate.m */; }; 517D70CE22BBC45900E374FB /* BAUptimeProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 51D9A2A21F3080A7004307A7 /* BAUptimeProvider.m */; }; - 517D70CF22BBC45900E374FB /* BANetworkParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849B4719B9F0490013408B /* BANetworkParameters.m */; }; 517D70D022BBC45900E374FB /* BAParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849B4919B9F0490013408B /* BAParameter.m */; }; 517D70D122BBC45900E374FB /* BAPropertiesCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849B4B19B9F0490013408B /* BAPropertiesCenter.m */; }; 517D70D222BBC45900E374FB /* BAUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 02849B4D19B9F0490013408B /* BAUserDefaults.m */; }; @@ -396,8 +397,7 @@ 517D70DC22BBC45900E374FB /* BASecureDate.m in Sources */ = {isa = PBXBuildFile; fileRef = 026C270219ED4A2A0079B969 /* BASecureDate.m */; }; 517D70DD22BBC45900E374FB /* BAErrorHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0284997A19B9EFBA0013408B /* BAErrorHelper.m */; }; 517D70DF22BBC4EB00E374FB /* BatchCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C7D2531B14A1740038FAD7 /* BatchCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 517D70E022BBC4EC00E374FB /* BatchEventData.h in Headers */ = {isa = PBXBuildFile; fileRef = 511F1E1521109D3800796AD9 /* BatchEventData.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 517D70E122BBC4EC00E374FB /* BatchUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 022AC42A19DAA36900BDC3E7 /* BatchUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 517D70E022BBC4EC00E374FB /* BatchEventAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 511F1E1521109D3800796AD9 /* BatchEventAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517D70E222BBC4EC00E374FB /* BatchLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 51F222AB1B4C16AC005DFBCE /* BatchLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517D70E322BBC4EC00E374FB /* BatchInbox.h in Headers */ = {isa = PBXBuildFile; fileRef = 518E38411EA8EB93009AB274 /* BatchInbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517D70E422BBC4EC00E374FB /* BatchPush.h in Headers */ = {isa = PBXBuildFile; fileRef = 0284998619B9EFBA0013408B /* BatchPush.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -406,9 +406,8 @@ 517D70E722BBC4EC00E374FB /* BatchMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = 512596C31CDCC73400AC934B /* BatchMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517D70E822BBC4EC00E374FB /* BatchMessagingModels.h in Headers */ = {isa = PBXBuildFile; fileRef = 930CAD6922283980001F5A7B /* BatchMessagingModels.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517D70E922BBC4EC00E374FB /* BatchActions.h in Headers */ = {isa = PBXBuildFile; fileRef = 514671BA1D646F7100C50113 /* BatchActions.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 517D70EA22BBC50000E374FB /* BatchEventDataPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 511F1E332110AA0F00796AD9 /* BatchEventDataPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 517D70EA22BBC50000E374FB /* BatchEventAttributesPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 511F1E332110AA0F00796AD9 /* BatchEventAttributesPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D70EB22BBC50000E374FB /* BatchInboxPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 5142AD891EBCAB1500B94027 /* BatchInboxPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 517D70EC22BBC50000E374FB /* BatchPushPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 51741C4D211C427E00AD03EB /* BatchPushPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D70ED22BBC50000E374FB /* BatchUserAttributePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9308969C2209E90A00B4CCAE /* BatchUserAttributePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D70EE22BBC50000E374FB /* BatchMessagingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 5188CD261D65FDBE0023D4BE /* BatchMessagingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D70EF22BBC50E00E374FB /* BANotificationCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 0284997D19B9EFBA0013408B /* BANotificationCenter.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -500,7 +499,7 @@ 517D715622BBC50E00E374FB /* BAUserSQLiteDatasource.h in Headers */ = {isa = PBXBuildFile; fileRef = 510153A61BED11C4009E8B16 /* BAUserSQLiteDatasource.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D715722BBC50E00E374FB /* BAUserDataOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 512354B51BFA2472001C5259 /* BAUserDataOperation.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D715822BBC50E00E374FB /* BAUserDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 511074691C208F6300BFC215 /* BAUserDataManager.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 517D715922BBC50E00E374FB /* BAUserDataEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = 512354BB1BFA30EB001C5259 /* BAUserDataEditor.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 517D715922BBC50E00E374FB /* BAInstallDataEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = 512354BB1BFA30EB001C5259 /* BAInstallDataEditor.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D715A22BBC50E00E374FB /* BAUserAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 517EAE411C1890540058D288 /* BAUserAttribute.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D715B22BBC50E00E374FB /* BAUserDataEnums.h in Headers */ = {isa = PBXBuildFile; fileRef = 51A31D811E27DC8B004CDD65 /* BAUserDataEnums.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D715C22BBC50E00E374FB /* BAUserDataServices.h in Headers */ = {isa = PBXBuildFile; fileRef = 511074631C208B7800BFC215 /* BAUserDataServices.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -529,7 +528,6 @@ 517D718722BBC50E00E374FB /* BAResponseHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849A4819B9EFBA0013408B /* BAResponseHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D718822BBC50E00E374FB /* BAGETWebserviceClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 518E384D1EAA5EF7009AB274 /* BAGETWebserviceClient.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D718922BBC50E00E374FB /* BAQueryWebserviceClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849A4A19B9EFBA0013408B /* BAQueryWebserviceClient.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 517D718B22BBC50E00E374FB /* BAWebserviceMetrics.h in Headers */ = {isa = PBXBuildFile; fileRef = 51C7D24E1B1359700038FAD7 /* BAWebserviceMetrics.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D718C22BBC50E00E374FB /* BALogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 5124663B1DAD125800B14F1E /* BALogger.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D718D22BBC50E00E374FB /* BALoggerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 5124663F1DAD129600B14F1E /* BALoggerProtocol.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D718F22BBC50E00E374FB /* BALoggerUnified.h in Headers */ = {isa = PBXBuildFile; fileRef = 512466451DAD1C2700B14F1E /* BALoggerUnified.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -555,7 +553,6 @@ 517D71A622BBC50F00E374FB /* BASystemDateProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 5155F79E1EF95E5B00929DD8 /* BASystemDateProvider.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D71A722BBC50F00E374FB /* BATZAwareDate.h in Headers */ = {isa = PBXBuildFile; fileRef = 5113B65C1F27497400CDB623 /* BATZAwareDate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D71A822BBC50F00E374FB /* BAUptimeProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 51D9A2A11F3080A7004307A7 /* BAUptimeProvider.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 517D71A922BBC50F00E374FB /* BANetworkParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849B4619B9F0490013408B /* BANetworkParameters.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D71AA22BBC50F00E374FB /* BAParameter.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849B4819B9F0490013408B /* BAParameter.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D71AB22BBC50F00E374FB /* BAPropertiesCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849B4A19B9F0490013408B /* BAPropertiesCenter.h */; settings = {ATTRIBUTES = (Private, ); }; }; 517D71AC22BBC50F00E374FB /* BAUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = 02849B4C19B9F0490013408B /* BAUserDefaults.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -580,19 +577,24 @@ 518D886D2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 518D886B2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 518D886E2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 518D886C2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.m */; }; 518D887023560C79007BF341 /* messagingAnalyticsDeduplicatingDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518D886F23560C79007BF341 /* messagingAnalyticsDeduplicatingDelegateTests.swift */; }; - 5190F1E8253495B30014E5BA /* Versions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5190F1CA253491530014E5BA /* Versions.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 518F17142BBD7E9C003122CF /* pushAuthorizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518F17132BBD7E9C003122CF /* pushAuthorizationTests.swift */; }; + 5190F1E8253495B30014E5BA /* Versions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5190F1CA253491530014E5BA /* Versions.h */; }; 5192784A25D1764D005C43B4 /* webviewJavascriptBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5192784925D1764D005C43B4 /* webviewJavascriptBridgeTests.swift */; }; 5192785025D17B80005C43B4 /* XCTest+BAPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5192784F25D17B80005C43B4 /* XCTest+BAPromise.swift */; }; 5193FC9722F0830900F5C9B9 /* BAURLSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 5193FC9422F0830800F5C9B9 /* BAURLSession.m */; }; 5193FC9822F0830900F5C9B9 /* BAURLSessionProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 5193FC9522F0830800F5C9B9 /* BAURLSessionProtocol.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5193FC9922F0830900F5C9B9 /* BAURLSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 5193FC9622F0830900F5C9B9 /* BAURLSession.h */; settings = {ATTRIBUTES = (Private, ); }; }; 519BC8EE22D5B98100C3BBF7 /* webserviceCryptorFactoryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 519BC8ED22D5B98100C3BBF7 /* webserviceCryptorFactoryTests.m */; }; - 519D342C2111F4BF00E5D2BE /* batchEventDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519D342A2111F4B300E5D2BE /* batchEventDataTests.swift */; }; + 519D342C2111F4BF00E5D2BE /* batchEventAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519D342A2111F4B300E5D2BE /* batchEventAttributesTests.swift */; }; + 519E69F32B923B6A001483C5 /* XCTest+Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E69F22B923B6A001483C5 /* XCTest+Injection.swift */; }; 51A214DA25CC0769003FD6A6 /* BATWebviewUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 51A214D825CC0769003FD6A6 /* BATWebviewUtils.h */; settings = {ATTRIBUTES = (Private, ); }; }; 51A214DB25CC0769003FD6A6 /* BATWebviewUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 51A214D925CC0769003FD6A6 /* BATWebviewUtils.m */; }; 51A214E625CC09D0003FD6A6 /* webviewUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A214E525CC09D0003FD6A6 /* webviewUtilsTests.swift */; }; 51A214F125CC608D003FD6A6 /* localCampaignsParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A214F025CC608D003FD6A6 /* localCampaignsParserTests.swift */; }; + 51A9204A2B7E083400E939AC /* FeedbackGenerators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A920492B7E083400E939AC /* FeedbackGenerators.swift */; }; + 51AB577A2BAC810700A1E72B /* profileOperationsSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB57792BAC810700A1E72B /* profileOperationsSerializerTests.swift */; }; 51ACA05D2166193D006FAD04 /* batchUserDiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51ACA05C2166193D006FAD04 /* batchUserDiffTests.swift */; }; + 51AD7F4D2BA4BCF9009F2F85 /* eventDataValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AD7F4C2BA4BCF9009F2F85 /* eventDataValidatorTests.swift */; }; 51B4F50025D3F76F0077E948 /* osHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4F4FF25D3F76F0077E948 /* osHelperTests.swift */; }; 51B4F50625D3FA3B0077E948 /* OperatingSystemVersion+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4F50525D3FA3B0077E948 /* OperatingSystemVersion+Equatable.swift */; }; 51B8074924003DF70006882D /* BATMessagePackReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 51B8074724003DF70006882D /* BATMessagePackReader.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -600,18 +602,33 @@ 51BD19262020759A00C2524D /* builtinActionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 51BD19252020759A00C2524D /* builtinActionsTest.m */; }; 51C6A47225CA99DF00B07AB6 /* batchTestsBootstrap.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C6A47125CA99DF00B07AB6 /* batchTestsBootstrap.m */; }; 51C6B11525CD53E200B531C5 /* localCampaignsCenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C6B11425CD53E200B531C5 /* localCampaignsCenterTests.m */; }; + 51CA52512BB5CB470093E2CA /* TestProfileEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CA52502BB5CB470093E2CA /* TestProfileEditor.swift */; }; + 51CADDCF2B88AD3B00D06BDE /* BATEventTrackerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CADDCE2B88AD3B00D06BDE /* BATEventTrackerProtocol.swift */; }; + 51CADDD12B88B77C00D06BDE /* BAInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CADDD02B88B77C00D06BDE /* BAInjection.swift */; }; + 51CADDDE2B88D83200D06BDE /* BatchProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 51CADDDC2B88D83200D06BDE /* BatchProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 51CADDDF2B88D83200D06BDE /* BatchProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 51CADDDD2B88D83200D06BDE /* BatchProfile.m */; }; 51D3E79E2328EF7100DF82AF /* BAWindowHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 51D3E79C2328EF7100DF82AF /* BAWindowHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; 51D3E79F2328EF7100DF82AF /* BAWindowHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 51D3E79D2328EF7100DF82AF /* BAWindowHelper.m */; }; 51D5779B24409EE000211582 /* messagePackReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5779A24409EE000211582 /* messagePackReaderTests.swift */; }; 51D5779D24409F3F00211582 /* DataUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5779C24409F3F00211582 /* DataUtils.swift */; }; + 51D9DD8B2B95F73D00F96F3D /* BATProfileEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D9DD8A2B95F73D00F96F3D /* BATProfileEditor.swift */; }; 51DD799E25D29FA6005E8D62 /* webviewBridgeWKHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD799D25D29FA6005E8D62 /* webviewBridgeWKHandlerTests.swift */; }; 51DD79BC25D2BE0F005E8D62 /* webviewBridgeLegacyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD79BB25D2BE0F005E8D62 /* webviewBridgeLegacyTests.swift */; }; 51DD79C225D2C5BE005E8D62 /* webviewTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD79C125D2C5BE005E8D62 /* webviewTestHelpers.swift */; }; - 51E22DC72451CA3C00D02CA8 /* batchUserEditorPublicAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E22DC62451CA3C00D02CA8 /* batchUserEditorPublicAPITests.m */; }; 51EB8A3723A3D97000879584 /* queryTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5175FE0A22F9D6E70090B907 /* queryTestHelpers.swift */; }; + 51EBCF662B8CF40700CA3C6F /* profileIdentifyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBCF652B8CF40700CA3C6F /* profileIdentifyTests.swift */; }; + 51EBCF6B2B8CF5D200CA3C6F /* EventTrackerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBCF6A2B8CF5D200CA3C6F /* EventTrackerMock.swift */; }; + 51EBCF742B8CFD4200CA3C6F /* BatchUserDataEditorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBCF732B8CFD4200CA3C6F /* BatchUserDataEditorMock.swift */; }; 51ECC06D22D4F8C800F1C5EC /* webserviceAESGCMCryptorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51ECC06C22D4F8C800F1C5EC /* webserviceAESGCMCryptorTests.m */; }; + 51F1E95F2B9A1D5500F90E06 /* BATProfileOperationsSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F1E95E2B9A1D5500F90E06 /* BATProfileOperationsSerializer.swift */; }; 51F1FCFC20DAA0A7006EDE43 /* batchTaskDebouncerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F1FCFB20DAA0A7006EDE43 /* batchTaskDebouncerTest.swift */; }; + 51F3D0D52B91F66E007EBEE3 /* userDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F3D0D42B91F66E007EBEE3 /* userDataManagerTests.swift */; }; + 51F3D0D72B91F697007EBEE3 /* mockUserDatasource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F3D0D62B91F697007EBEE3 /* mockUserDatasource.swift */; }; + 51F3D0DA2B923434007EBEE3 /* BatchProfileEditor.h in Headers */ = {isa = PBXBuildFile; fileRef = 51F3D0D82B923434007EBEE3 /* BatchProfileEditor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 51F3D0DB2B923434007EBEE3 /* BatchProfileEditor.m in Sources */ = {isa = PBXBuildFile; fileRef = 51F3D0D92B923434007EBEE3 /* BatchProfileEditor.m */; }; 51F58E36257FAA65000DE3E0 /* BAMessagingAnalyticsDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 51F58E35257FAA31000DE3E0 /* BAMessagingAnalyticsDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 51F5B2E72BBD58C0006E2790 /* XCTest+Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F5B2E62BBD58C0006E2790 /* XCTest+Threading.swift */; }; + 51F6CA542B979D0D005C9F29 /* BatchProfileError+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6CA532B979D0D005C9F29 /* BatchProfileError+Init.swift */; }; 51F91A2022DF1B00003A42F1 /* BAWebserviceURLBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 51F91A1E22DF1B00003A42F1 /* BAWebserviceURLBuilder.h */; settings = {ATTRIBUTES = (Private, ); }; }; 51F91A2122DF1B00003A42F1 /* BAWebserviceURLBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 51F91A1F22DF1B00003A42F1 /* BAWebserviceURLBuilder.m */; }; 51FCC9332577EF9C00FA76C6 /* BAMSGActivityIndicatorView.h in Headers */ = {isa = PBXBuildFile; fileRef = 51FCC9312577EF9C00FA76C6 /* BAMSGActivityIndicatorView.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -663,6 +680,14 @@ 62DEADCE24570BC60065301F /* BADisplayReceiptWebserviceClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 62DEADCD24570BC60065301F /* BADisplayReceiptWebserviceClient.m */; }; 62F61A962435D78500652B29 /* BAUserEventBuiltinActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F61A952435D78500652B29 /* BAUserEventBuiltinActions.m */; }; 62F61A982435D79C00652B29 /* BAUserEventBuiltinActions.h in Headers */ = {isa = PBXBuildFile; fileRef = 62F61A972435D79C00652B29 /* BAUserEventBuiltinActions.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 7E01A6442BC5410100B0105B /* BatchDataCollectionConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E01A6432BC5410100B0105B /* BatchDataCollectionConfig.m */; }; + 7E01A6452BC5410100B0105B /* BatchDataCollectionConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E01A6422BC5410100B0105B /* BatchDataCollectionConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7E39F0142BC5904E009FC9E7 /* DataCollectionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E39F0132BC5904E009FC9E7 /* DataCollectionUtils.swift */; }; + 7E39F01D2BC96849009FC9E7 /* profileMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E39F01C2BC96849009FC9E7 /* profileMigrationTests.swift */; }; + 7E39F0242BC97133009FC9E7 /* TestProfileCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E39F0232BC97133009FC9E7 /* TestProfileCenter.swift */; }; + 7E53722B2BBC384D00F2E78A /* BATDataCollectionCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E53722A2BBC384D00F2E78A /* BATDataCollectionCenter.swift */; }; + 7E53722D2BBC38BC00F2E78A /* BATSystemParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E53722C2BBC38BC00F2E78A /* BATSystemParameter.swift */; }; + 7ED8D8452BBED42E00CAF988 /* dataCollectionCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ED8D8442BBED42E00CAF988 /* dataCollectionCenterTests.swift */; }; 9323C51E2225A1960097645F /* htmlParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9323C51D2225A1960097645F /* htmlParserTests.m */; }; 9373ECDD2211C9D800FA00A3 /* DeeplinkDelegateStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 9373ECDC2211C9D800FA00A3 /* DeeplinkDelegateStub.m */; }; 939E058A222D2CC700E8D32D /* batchUserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 93C78F5E220AE4120082A6F7 /* batchUserTests.m */; }; @@ -692,8 +717,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 022AC42A19DAA36900BDC3E7 /* BatchUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BatchUserProfile.h; path = Batch/BatchUserProfile.h; sourceTree = SOURCE_ROOT; }; - 022AC42B19DAA36900BDC3E7 /* BatchUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BatchUserProfile.m; path = Batch/BatchUserProfile.m; sourceTree = SOURCE_ROOT; }; 022AC43019DAA8EA00BDC3E7 /* BAUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAUserProfile.h; path = Batch/Modules/Core/BAUserProfile.h; sourceTree = SOURCE_ROOT; }; 022AC43119DAA8EA00BDC3E7 /* BAUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAUserProfile.m; path = Batch/Modules/Core/BAUserProfile.m; sourceTree = SOURCE_ROOT; }; 026C270219ED4A2A0079B969 /* BASecureDate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BASecureDate.m; path = Batch/BASecureDate.m; sourceTree = SOURCE_ROOT; }; @@ -744,8 +767,6 @@ 02849B3F19B9F0490013408B /* BAEncryptionProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAEncryptionProtocol.h; sourceTree = ""; }; 02849B4319B9F0490013408B /* BAMultiDelegatesProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAMultiDelegatesProxy.h; sourceTree = ""; }; 02849B4419B9F0490013408B /* BAMultiDelegatesProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BAMultiDelegatesProxy.m; sourceTree = ""; }; - 02849B4619B9F0490013408B /* BANetworkParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BANetworkParameters.h; sourceTree = ""; }; - 02849B4719B9F0490013408B /* BANetworkParameters.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BANetworkParameters.m; sourceTree = ""; }; 02849B4819B9F0490013408B /* BAParameter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAParameter.h; sourceTree = ""; }; 02849B4919B9F0490013408B /* BAParameter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BAParameter.m; sourceTree = ""; }; 02849B4A19B9F0490013408B /* BAPropertiesCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAPropertiesCenter.h; sourceTree = ""; }; @@ -772,7 +793,6 @@ 02849BE419B9FA2D0013408B /* batchTrackerCenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = batchTrackerCenterTests.m; sourceTree = ""; }; 02849BF719B9FA2D0013408B /* batchWebserviceResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = batchWebserviceResponseTests.m; sourceTree = ""; }; 02849BFA19B9FA2D0013408B /* batchResponseHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = batchResponseHelperTests.m; sourceTree = ""; }; - 02955F5D19DAE0E800E7458A /* batchUserProfileTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = batchUserProfileTests.m; path = batchTests/Modules/Core/batchUserProfileTests.m; sourceTree = SOURCE_ROOT; }; 029B33481A39A1F7006B9DC6 /* batchNSURLREquest+BAHeadersTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "batchNSURLREquest+BAHeadersTests.m"; sourceTree = ""; }; 029B334A1A39A1F7006B9DC6 /* batchAESTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = batchAESTests.m; sourceTree = ""; }; 029B334E1A39A1F7006B9DC6 /* batchParametersTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = batchParametersTests.m; sourceTree = ""; }; @@ -781,9 +801,6 @@ 02F7846419B9EF5A0006FF07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 02F7846B19B9EF5A0006FF07 /* BatchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BatchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02F7846E19B9EF5A0006FF07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 1E104EAD28E6F43D00E998AE /* BAUserEmailSubscription.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAUserEmailSubscription.h; sourceTree = ""; }; - 1E104EAE28E6F43D00E998AE /* BAUserEmailSubscription.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAUserEmailSubscription.m; sourceTree = ""; }; - 1E104EB528EB289400E998AE /* batchUserEmailSubscriptionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = batchUserEmailSubscriptionTests.m; path = User/batchUserEmailSubscriptionTests.m; sourceTree = ""; }; 1E2C31B327CF740E00A4CB6E /* BAMetricRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMetricRegistry.h; sourceTree = ""; }; 1E2C31B427CF740E00A4CB6E /* BAMetricRegistry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAMetricRegistry.m; sourceTree = ""; }; 1E45578327CE1B510048D3A8 /* BALocalCampaignsJITService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BALocalCampaignsJITService.h; sourceTree = ""; }; @@ -797,9 +814,7 @@ 1EA42DF726A84C71004CC618 /* BADBGFindMyInstallationHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BADBGFindMyInstallationHelper.m; sourceTree = ""; }; 1EA42DF926A84C99004CC618 /* BADBGFindMyInstallationHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BADBGFindMyInstallationHelper.h; sourceTree = ""; }; 1EA42E0226A9A716004CC618 /* FindMyInstallationHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FindMyInstallationHelperTests.m; sourceTree = ""; }; - 1EB69D39290C09B9003E1EF7 /* BAEmailUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAEmailUtils.h; sourceTree = ""; }; - 1EB69D3A290C09B9003E1EF7 /* BAEmailUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAEmailUtils.m; sourceTree = ""; }; - 1EB69D3D290C0F5A003E1EF7 /* emailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = emailTests.swift; sourceTree = ""; }; + 1EB69D3D290C0F5A003E1EF7 /* profileEditorValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = profileEditorValidationTests.swift; sourceTree = ""; }; 1EBDBC6127157A3A006DC662 /* BACounter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BACounter.h; sourceTree = ""; }; 1EBDBC6227157A3A006DC662 /* BACounter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BACounter.m; sourceTree = ""; }; 1EBDBC652715B1B5006DC662 /* BAMetricProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMetricProtocol.h; sourceTree = ""; }; @@ -834,6 +849,7 @@ 51022DB01D23DA9700EDAA5C /* BAMSGStackViewHorizontalItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAMSGStackViewHorizontalItem.h; path = Batch/Modules/Messaging/UI/BAMSGStackViewHorizontalItem.h; sourceTree = SOURCE_ROOT; }; 51022DB11D23DA9700EDAA5C /* BAMSGStackViewHorizontalItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGStackViewHorizontalItem.m; path = Batch/Modules/Messaging/UI/BAMSGStackViewHorizontalItem.m; sourceTree = SOURCE_ROOT; }; 5103E4491F97B09E00155F6A /* BAMSGWindowHolder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMSGWindowHolder.h; sourceTree = ""; }; + 510737412BC596950033E2E8 /* profileEditorCompatibilityTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = profileEditorCompatibilityTest.m; sourceTree = ""; }; 510BD5BD22E0B4BA00D8CBA1 /* BAWSResponseAttributesCheck.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BAWSResponseAttributesCheck.m; sourceTree = ""; }; 510BD5BE22E0B4BA00D8CBA1 /* BAWSResponseAttributes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BAWSResponseAttributes.m; sourceTree = ""; }; 510BD5BF22E0B4BA00D8CBA1 /* BAWSResponseAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAWSResponseAttributes.h; sourceTree = ""; }; @@ -851,6 +867,10 @@ 510BD5D322E0B4D500D8CBA1 /* BAWSQueryStart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAWSQueryStart.h; sourceTree = ""; }; 510BD5D422E0B4D500D8CBA1 /* BAWSQueryStart.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BAWSQueryStart.m; sourceTree = ""; }; 510D906E24C83A81005981AC /* shaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = shaTests.swift; sourceTree = ""; }; + 510F99252B8E2F35009D15DE /* BATSDKError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATSDKError.swift; sourceTree = ""; }; + 510F99272B8E36EE009D15DE /* BATRegularExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATRegularExpression.swift; sourceTree = ""; }; + 510F99292B8E9192009D15DE /* BATEventAttributesValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATEventAttributesValidator.swift; sourceTree = ""; }; + 510F99352B8F8544009D15DE /* BATEventAttributesSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATEventAttributesSerializer.swift; sourceTree = ""; }; 510FFEE622FD7FE500BBF277 /* dependencyInjectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dependencyInjectionTests.swift; sourceTree = ""; }; 51103A782A7405B40082987C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 511074631C208B7800BFC215 /* BAUserDataServices.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAUserDataServices.h; sourceTree = ""; }; @@ -867,14 +887,14 @@ 511B920A22CE4400002BC4A8 /* BAWebserviceStubCryptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAWebserviceStubCryptor.m; sourceTree = ""; }; 511B920D22CE44F9002BC4A8 /* BAWebserviceAESGCMCryptor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAWebserviceAESGCMCryptor.h; sourceTree = ""; }; 511B920E22CE44F9002BC4A8 /* BAWebserviceAESGCMCryptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAWebserviceAESGCMCryptor.m; sourceTree = ""; }; - 511F1E1521109D3800796AD9 /* BatchEventData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchEventData.h; sourceTree = ""; }; - 511F1E1621109D3800796AD9 /* BatchEventData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BatchEventData.m; sourceTree = ""; }; - 511F1E332110AA0F00796AD9 /* BatchEventDataPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchEventDataPrivate.h; sourceTree = ""; }; + 511F1E1521109D3800796AD9 /* BatchEventAttributes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchEventAttributes.h; sourceTree = ""; }; + 511F1E1621109D3800796AD9 /* BatchEventAttributes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BatchEventAttributes.m; sourceTree = ""; }; + 511F1E332110AA0F00796AD9 /* BatchEventAttributesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchEventAttributesPrivate.h; sourceTree = ""; }; 5122032522CA5C0B001A505F /* cssTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cssTests.swift; sourceTree = ""; }; 512354B51BFA2472001C5259 /* BAUserDataOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAUserDataOperation.h; path = Batch/Modules/User/BAUserDataOperation.h; sourceTree = SOURCE_ROOT; }; 512354B61BFA2472001C5259 /* BAUserDataOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAUserDataOperation.m; path = Batch/Modules/User/BAUserDataOperation.m; sourceTree = SOURCE_ROOT; }; - 512354BB1BFA30EB001C5259 /* BAUserDataEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAUserDataEditor.h; path = Batch/Modules/User/BAUserDataEditor.h; sourceTree = SOURCE_ROOT; }; - 512354BC1BFA30EB001C5259 /* BAUserDataEditor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAUserDataEditor.m; path = Batch/Modules/User/BAUserDataEditor.m; sourceTree = SOURCE_ROOT; }; + 512354BB1BFA30EB001C5259 /* BAInstallDataEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAInstallDataEditor.h; path = Batch/Modules/User/BAInstallDataEditor.h; sourceTree = SOURCE_ROOT; }; + 512354BC1BFA30EB001C5259 /* BAInstallDataEditor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAInstallDataEditor.m; path = Batch/Modules/User/BAInstallDataEditor.m; sourceTree = SOURCE_ROOT; }; 51242703244D8F9700025DA3 /* batchUserDataEditorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = batchUserDataEditorTests.swift; sourceTree = ""; }; 5124663B1DAD125800B14F1E /* BALogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BALogger.h; path = Batch/Kernel/Logger/BALogger.h; sourceTree = SOURCE_ROOT; }; 5124663C1DAD125800B14F1E /* BALogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BALogger.m; path = Batch/Kernel/Logger/BALogger.m; sourceTree = SOURCE_ROOT; }; @@ -921,6 +941,7 @@ 514671C11D647AFE00C50113 /* BAActionsCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAActionsCenter.h; path = Batch/Modules/Actions/BAActionsCenter.h; sourceTree = SOURCE_ROOT; }; 514671C21D647AFE00C50113 /* BAActionsCenter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAActionsCenter.m; path = Batch/Modules/Actions/BAActionsCenter.m; sourceTree = SOURCE_ROOT; }; 51496A2E25110D680051B8A4 /* BAPartialApplicationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAPartialApplicationDelegate.h; sourceTree = ""; }; + 514B39A72BAA095D006980EB /* eventDataSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = eventDataSerializerTests.swift; sourceTree = ""; }; 514BA49F27D257F100B93FAF /* inboxFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = inboxFetcherTests.swift; sourceTree = ""; }; 514CD72A1BFE2A2B00F79110 /* batchUserDataSQLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = batchUserDataSQLTests.m; path = batchTests/Modules/User/batchUserDataSQLTests.m; sourceTree = SOURCE_ROOT; }; 514FCA7325121DDF00CAF31A /* OCMock.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = OCMock.modulemap; sourceTree = ""; }; @@ -1058,10 +1079,13 @@ 516BE208214819160057D937 /* deeplinkDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = deeplinkDelegateTests.m; sourceTree = ""; }; 516D31C12445C40400BF3AB0 /* messagePackWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = messagePackWriterTests.swift; sourceTree = ""; }; 516D7117260924CC00CF7F5B /* libOCMock-Batch.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libOCMock-Batch.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 516D98AA2BC05A0C00345778 /* BATProfileInstallDataCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATProfileInstallDataCompatibility.swift; sourceTree = ""; }; 51711ABF23D60A5300FF2B9D /* PrivateUmbrellaHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivateUmbrellaHeader.h; sourceTree = ""; }; 5171445922DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAStandardQueryWebserviceIdentifiersProvider.h; sourceTree = ""; }; 5171445A22DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAStandardQueryWebserviceIdentifiersProvider.m; sourceTree = ""; }; - 51741C4D211C427E00AD03EB /* BatchPushPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchPushPrivate.h; sourceTree = ""; }; + 5172DD0B2B88E2620094784D /* BAProfileCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BAProfileCenter.swift; sourceTree = ""; }; + 5172DD102B88EE230094784D /* BATInternalEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATInternalEvents.swift; sourceTree = ""; }; + 5172DD122B88F7B80094784D /* BATProfileDataValidators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATProfileDataValidators.swift; sourceTree = ""; }; 5175FE0A22F9D6E70090B907 /* queryTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = queryTestHelpers.swift; sourceTree = ""; }; 51792F91201B8926001AF271 /* BAUserDataBuiltinActions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAUserDataBuiltinActions.h; sourceTree = ""; }; 51792F92201B8926001AF271 /* BAUserDataBuiltinActions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAUserDataBuiltinActions.m; sourceTree = ""; }; @@ -1102,6 +1126,7 @@ 518E384E1EAA5EF7009AB274 /* BAGETWebserviceClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAGETWebserviceClient.m; path = Batch/Webservices/BAGETWebserviceClient.m; sourceTree = SOURCE_ROOT; }; 518E385F1EAF8C43009AB274 /* BAJson.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAJson.h; path = Batch/Kernel/Helpers/BAJson.h; sourceTree = SOURCE_ROOT; }; 518E38601EAF8C43009AB274 /* BAJson.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAJson.m; path = Batch/Kernel/Helpers/BAJson.m; sourceTree = SOURCE_ROOT; }; + 518F17132BBD7E9C003122CF /* pushAuthorizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = pushAuthorizationTests.swift; sourceTree = ""; }; 5190F1CA253491530014E5BA /* Versions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Versions.h; sourceTree = ""; }; 5192784925D1764D005C43B4 /* webviewJavascriptBridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = webviewJavascriptBridgeTests.swift; sourceTree = ""; }; 5192784F25D17B80005C43B4 /* XCTest+BAPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+BAPromise.swift"; sourceTree = ""; }; @@ -1117,7 +1142,8 @@ 519834981F5850EC002DCBD9 /* BAMSGBaseContainerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMSGBaseContainerView.h; sourceTree = ""; }; 519834991F5850EC002DCBD9 /* BAMSGBaseContainerView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAMSGBaseContainerView.m; sourceTree = ""; }; 519BC8ED22D5B98100C3BBF7 /* webserviceCryptorFactoryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = webserviceCryptorFactoryTests.m; sourceTree = ""; }; - 519D342A2111F4B300E5D2BE /* batchEventDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = batchEventDataTests.swift; sourceTree = ""; }; + 519D342A2111F4B300E5D2BE /* batchEventAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = batchEventAttributesTests.swift; sourceTree = ""; }; + 519E69F22B923B6A001483C5 /* XCTest+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Injection.swift"; sourceTree = ""; }; 519EC1891F1E5FF50040C432 /* BANextSessionTrigger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BANextSessionTrigger.h; sourceTree = ""; }; 519EC18A1F1E5FF50040C432 /* BANextSessionTrigger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BANextSessionTrigger.m; sourceTree = ""; }; 51A214D825CC0769003FD6A6 /* BATWebviewUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BATWebviewUtils.h; sourceTree = ""; }; @@ -1132,9 +1158,12 @@ 51A31D811E27DC8B004CDD65 /* BAUserDataEnums.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BAUserDataEnums.h; sourceTree = ""; }; 51A41A221D059D4300187BC6 /* BAMSGGradientView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAMSGGradientView.h; path = Batch/Modules/Messaging/Widgets/BAMSGGradientView.h; sourceTree = SOURCE_ROOT; }; 51A41A231D059D4300187BC6 /* BAMSGGradientView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGGradientView.m; path = Batch/Modules/Messaging/Widgets/BAMSGGradientView.m; sourceTree = SOURCE_ROOT; }; + 51A920492B7E083400E939AC /* FeedbackGenerators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackGenerators.swift; sourceTree = ""; }; 51A939A11D1ABC4D00AACEAF /* BAMSGVideoView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAMSGVideoView.h; path = Batch/Modules/Messaging/UI/BAMSGVideoView.h; sourceTree = SOURCE_ROOT; }; 51A939A21D1ABC4D00AACEAF /* BAMSGVideoView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGVideoView.m; path = Batch/Modules/Messaging/UI/BAMSGVideoView.m; sourceTree = SOURCE_ROOT; }; + 51AB57792BAC810700A1E72B /* profileOperationsSerializerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = profileOperationsSerializerTests.swift; sourceTree = ""; }; 51ACA05C2166193D006FAD04 /* batchUserDiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = batchUserDiffTests.swift; path = User/batchUserDiffTests.swift; sourceTree = ""; }; + 51AD7F4C2BA4BCF9009F2F85 /* eventDataValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = eventDataValidatorTests.swift; sourceTree = ""; }; 51AE74612084AEE3005186D7 /* BAOptOut.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAOptOut.h; sourceTree = ""; }; 51AE74622084AEE3005186D7 /* BAOptOut.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAOptOut.m; sourceTree = ""; }; 51AE746620850217005186D7 /* BAInstallationID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAInstallationID.h; sourceTree = ""; }; @@ -1161,8 +1190,6 @@ 51BFA5441C23165E00CA218C /* BAUserCenter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAUserCenter.m; path = Batch/Modules/User/BAUserCenter.m; sourceTree = SOURCE_ROOT; }; 51C6A47125CA99DF00B07AB6 /* batchTestsBootstrap.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = batchTestsBootstrap.m; sourceTree = ""; }; 51C6B11425CD53E200B531C5 /* localCampaignsCenterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = localCampaignsCenterTests.m; sourceTree = ""; }; - 51C7D24E1B1359700038FAD7 /* BAWebserviceMetrics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAWebserviceMetrics.h; path = Batch/Webservices/BAWebserviceMetrics.h; sourceTree = SOURCE_ROOT; }; - 51C7D24F1B1359700038FAD7 /* BAWebserviceMetrics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAWebserviceMetrics.m; path = Batch/Webservices/BAWebserviceMetrics.m; sourceTree = SOURCE_ROOT; }; 51C7D2531B14A1740038FAD7 /* BatchCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BatchCore.h; path = Batch/BatchCore.h; sourceTree = SOURCE_ROOT; }; 51C7F6A81F615832003F2215 /* BAMSGPannableAnchoredContainerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMSGPannableAnchoredContainerView.h; sourceTree = ""; }; 51C7F6A91F615832003F2215 /* BAMSGPannableAnchoredContainerView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAMSGPannableAnchoredContainerView.m; sourceTree = ""; }; @@ -1171,6 +1198,11 @@ 51C9D1F51F1CF7F1002583E9 /* BALocalCampaignsParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BALocalCampaignsParser.h; sourceTree = ""; }; 51C9D1F61F1CF7F1002583E9 /* BALocalCampaignsParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BALocalCampaignsParser.m; sourceTree = ""; }; 51C9D1FE1F1D0552002583E9 /* BALocalCampaignOutputProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BALocalCampaignOutputProtocol.h; sourceTree = ""; }; + 51CA52502BB5CB470093E2CA /* TestProfileEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestProfileEditor.swift; sourceTree = ""; }; + 51CADDCE2B88AD3B00D06BDE /* BATEventTrackerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATEventTrackerProtocol.swift; sourceTree = ""; }; + 51CADDD02B88B77C00D06BDE /* BAInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BAInjection.swift; sourceTree = ""; }; + 51CADDDC2B88D83200D06BDE /* BatchProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BatchProfile.h; sourceTree = ""; }; + 51CADDDD2B88D83200D06BDE /* BatchProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BatchProfile.m; sourceTree = ""; }; 51D3A8A51EB24A1900C77463 /* BAInboxFetchWebserviceClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAInboxFetchWebserviceClient.h; path = Batch/Modules/Inbox/BAInboxFetchWebserviceClient.h; sourceTree = SOURCE_ROOT; }; 51D3A8A61EB24A1900C77463 /* BAInboxFetchWebserviceClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAInboxFetchWebserviceClient.m; path = Batch/Modules/Inbox/BAInboxFetchWebserviceClient.m; sourceTree = SOURCE_ROOT; }; 51D3A8B41EB8DA4800C77463 /* BATJsonDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BATJsonDictionary.h; path = Batch/Kernel/Helpers/BATJsonDictionary.h; sourceTree = SOURCE_ROOT; }; @@ -1192,6 +1224,7 @@ 51D9987B26024B3F00E31434 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = SOURCE_ROOT; }; 51D9A2A11F3080A7004307A7 /* BAUptimeProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAUptimeProvider.h; sourceTree = ""; }; 51D9A2A21F3080A7004307A7 /* BAUptimeProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAUptimeProvider.m; sourceTree = ""; }; + 51D9DD8A2B95F73D00F96F3D /* BATProfileEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATProfileEditor.swift; sourceTree = ""; }; 51DB2A15213ECE1B005AAA4F /* BAEventTrackedSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAEventTrackedSignal.h; sourceTree = ""; }; 51DB2A16213ECE1B005AAA4F /* BAEventTrackedSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAEventTrackedSignal.m; sourceTree = ""; }; 51DD799D25D29FA6005E8D62 /* webviewBridgeWKHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = webviewBridgeWKHandlerTests.swift; sourceTree = ""; }; @@ -1201,7 +1234,6 @@ 51E042AE200CF85A00B30D49 /* BADBGCustomDataViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BADBGCustomDataViewController.m; sourceTree = ""; }; 51E042B1200D000800B30D49 /* BADBGCustomDataModels.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BADBGCustomDataModels.h; sourceTree = ""; }; 51E042B2200D000800B30D49 /* BADBGCustomDataModels.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BADBGCustomDataModels.m; sourceTree = ""; }; - 51E22DC62451CA3C00D02CA8 /* batchUserEditorPublicAPITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = batchUserEditorPublicAPITests.m; path = User/batchUserEditorPublicAPITests.m; sourceTree = ""; }; 51E445471D787DAF0075CAFD /* BAPushSystemHelperProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BAPushSystemHelperProtocol.h; path = Batch/Modules/Push/BAPushSystemHelperProtocol.h; sourceTree = SOURCE_ROOT; }; 51E445481D7883C50075CAFD /* BAPushSystemHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAPushSystemHelper.h; path = Batch/Modules/Push/BAPushSystemHelper.h; sourceTree = SOURCE_ROOT; }; 51E445491D7883C50075CAFD /* BAPushSystemHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAPushSystemHelper.m; path = Batch/Modules/Push/BAPushSystemHelper.m; sourceTree = SOURCE_ROOT; }; @@ -1221,6 +1253,9 @@ 51E666191CE3554C00F00E63 /* BAMSGImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGImageView.m; path = Batch/Modules/Messaging/Widgets/BAMSGImageView.m; sourceTree = SOURCE_ROOT; }; 51EAD7E31F62871A000B7B72 /* BAMSGCountdownView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMSGCountdownView.h; sourceTree = ""; }; 51EAD7E41F62871A000B7B72 /* BAMSGCountdownView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAMSGCountdownView.m; sourceTree = ""; }; + 51EBCF652B8CF40700CA3C6F /* profileIdentifyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = profileIdentifyTests.swift; sourceTree = ""; }; + 51EBCF6A2B8CF5D200CA3C6F /* EventTrackerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTrackerMock.swift; sourceTree = ""; }; + 51EBCF732B8CFD4200CA3C6F /* BatchUserDataEditorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchUserDataEditorMock.swift; sourceTree = ""; }; 51ECC06C22D4F8C800F1C5EC /* webserviceAESGCMCryptorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = webserviceAESGCMCryptorTests.m; sourceTree = ""; }; 51ECF37520065E490086C9CC /* BADBGDebugViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BADBGDebugViewController.h; sourceTree = ""; }; 51ECF37620065E490086C9CC /* BADBGDebugViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BADBGDebugViewController.m; sourceTree = ""; }; @@ -1250,12 +1285,19 @@ 51F072281CDA4D980019707A /* BAMSGLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGLabel.m; path = Batch/Modules/Messaging/Widgets/BAMSGLabel.m; sourceTree = SOURCE_ROOT; }; 51F072291CDA4D980019707A /* BAMSGStylableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BAMSGStylableView.h; path = Batch/Modules/Messaging/Widgets/BAMSGStylableView.h; sourceTree = SOURCE_ROOT; }; 51F0722A1CDA4D980019707A /* BAMSGStylableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BAMSGStylableView.m; path = Batch/Modules/Messaging/Widgets/BAMSGStylableView.m; sourceTree = SOURCE_ROOT; }; + 51F1E95E2B9A1D5500F90E06 /* BATProfileOperationsSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATProfileOperationsSerializer.swift; sourceTree = ""; }; 51F1FCF320DA954D006EDE43 /* BATaskDebouncer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BATaskDebouncer.h; sourceTree = ""; }; 51F1FCF420DA954D006EDE43 /* BATaskDebouncer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BATaskDebouncer.m; sourceTree = ""; }; 51F1FCFA20DAA0A6006EDE43 /* batchTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "batchTests-Bridging-Header.h"; sourceTree = ""; }; 51F1FCFB20DAA0A7006EDE43 /* batchTaskDebouncerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = batchTaskDebouncerTest.swift; sourceTree = ""; }; 51F222AB1B4C16AC005DFBCE /* BatchLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BatchLogger.h; path = Batch/BatchLogger.h; sourceTree = SOURCE_ROOT; }; + 51F3D0D42B91F66E007EBEE3 /* userDataManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = userDataManagerTests.swift; path = User/userDataManagerTests.swift; sourceTree = ""; }; + 51F3D0D62B91F697007EBEE3 /* mockUserDatasource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mockUserDatasource.swift; sourceTree = ""; }; + 51F3D0D82B923434007EBEE3 /* BatchProfileEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BatchProfileEditor.h; sourceTree = ""; }; + 51F3D0D92B923434007EBEE3 /* BatchProfileEditor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BatchProfileEditor.m; sourceTree = ""; }; 51F58E35257FAA31000DE3E0 /* BAMessagingAnalyticsDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMessagingAnalyticsDelegate.h; sourceTree = ""; }; + 51F5B2E62BBD58C0006E2790 /* XCTest+Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Threading.swift"; sourceTree = ""; }; + 51F6CA532B979D0D005C9F29 /* BatchProfileError+Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BatchProfileError+Init.swift"; sourceTree = ""; }; 51F91A1E22DF1B00003A42F1 /* BAWebserviceURLBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAWebserviceURLBuilder.h; sourceTree = ""; }; 51F91A1F22DF1B00003A42F1 /* BAWebserviceURLBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAWebserviceURLBuilder.m; sourceTree = ""; }; 51FCC9312577EF9C00FA76C6 /* BAMSGActivityIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAMSGActivityIndicatorView.h; sourceTree = ""; }; @@ -1309,6 +1351,14 @@ 62DEADCD24570BC60065301F /* BADisplayReceiptWebserviceClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BADisplayReceiptWebserviceClient.m; sourceTree = ""; }; 62F61A952435D78500652B29 /* BAUserEventBuiltinActions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BAUserEventBuiltinActions.m; sourceTree = ""; }; 62F61A972435D79C00652B29 /* BAUserEventBuiltinActions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BAUserEventBuiltinActions.h; sourceTree = ""; }; + 7E01A6422BC5410100B0105B /* BatchDataCollectionConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchDataCollectionConfig.h; sourceTree = ""; }; + 7E01A6432BC5410100B0105B /* BatchDataCollectionConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BatchDataCollectionConfig.m; sourceTree = ""; }; + 7E39F0132BC5904E009FC9E7 /* DataCollectionUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCollectionUtils.swift; sourceTree = ""; }; + 7E39F01C2BC96849009FC9E7 /* profileMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = profileMigrationTests.swift; sourceTree = ""; }; + 7E39F0232BC97133009FC9E7 /* TestProfileCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestProfileCenter.swift; sourceTree = ""; }; + 7E53722A2BBC384D00F2E78A /* BATDataCollectionCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATDataCollectionCenter.swift; sourceTree = ""; }; + 7E53722C2BBC38BC00F2E78A /* BATSystemParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BATSystemParameter.swift; sourceTree = ""; }; + 7ED8D8442BBED42E00CAF988 /* dataCollectionCenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dataCollectionCenterTests.swift; sourceTree = ""; }; 930896982209E47700B4CCAE /* BatchUserAttribute.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchUserAttribute.h; sourceTree = ""; }; 930896992209E47700B4CCAE /* BatchUserAttribute.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BatchUserAttribute.m; sourceTree = ""; }; 9308969C2209E90A00B4CCAE /* BatchUserAttributePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatchUserAttributePrivate.h; sourceTree = ""; }; @@ -1378,10 +1428,12 @@ 028499F219B9EFBA0013408B /* Modules */ = { isa = PBXGroup; children = ( + 7E5372292BBC37A400F2E78A /* Data Collection */, 620F174424488AFB00D046D5 /* Display Receipt */, 625870302354C9CA006B0287 /* Event Dispatcher */, 028499F319B9EFBA0013408B /* Core */, 5155F7AA1EF95E6E00929DD8 /* Local Campaigns */, + 5172DD0A2B88E2510094784D /* Profile */, 028499FC19B9EFBA0013408B /* Push */, 02849A0319B9EFBA0013408B /* Tracker */, 1EF8BA9727145FDA0063E117 /* Metrics */, @@ -1418,6 +1470,7 @@ 51B5E4881C849D8900047F6E /* BABundleInfo.m */, 51AE746620850217005186D7 /* BAInstallationID.h */, 51AE746720850217005186D7 /* BAInstallationID.m */, + 5172DD102B88EE230094784D /* BATInternalEvents.swift */, 5157117029DAD6EC0002A9CD /* BATUserActivity.h */, 5157117129DAD6EC0002A9CD /* BATUserActivity.m */, ); @@ -1457,6 +1510,7 @@ 02849A0E19B9EFBA0013408B /* BATrackerSender.m */, 51977EDE20DCF9FB0046B40D /* BATrackerSignpostHelper.h */, 51977EDF20DCF9FB0046B40D /* BATrackerSignpostHelper.m */, + 51CADDCE2B88AD3B00D06BDE /* BATEventTrackerProtocol.swift */, ); path = Tracker; sourceTree = ""; @@ -1511,8 +1565,6 @@ 02849A4919B9EFBA0013408B /* BAResponseHelper.m */, 518E384D1EAA5EF7009AB274 /* BAGETWebserviceClient.h */, 518E384E1EAA5EF7009AB274 /* BAGETWebserviceClient.m */, - 51C7D24E1B1359700038FAD7 /* BAWebserviceMetrics.h */, - 51C7D24F1B1359700038FAD7 /* BAWebserviceMetrics.m */, 51F91A1E22DF1B00003A42F1 /* BAWebserviceURLBuilder.h */, 51F91A1F22DF1B00003A42F1 /* BAWebserviceURLBuilder.m */, 510BD5BA22E0A64800D8CBA1 /* Core */, @@ -1582,6 +1634,8 @@ 5113B65D1F27497400CDB623 /* BATZAwareDate.m */, 51D9A2A11F3080A7004307A7 /* BAUptimeProvider.h */, 51D9A2A21F3080A7004307A7 /* BAUptimeProvider.m */, + 510F99252B8E2F35009D15DE /* BATSDKError.swift */, + 510F99272B8E36EE009D15DE /* BATRegularExpression.swift */, ); path = Foundation; sourceTree = ""; @@ -1589,8 +1643,6 @@ 02849B4519B9F0490013408B /* Parameters */ = { isa = PBXGroup; children = ( - 02849B4619B9F0490013408B /* BANetworkParameters.h */, - 02849B4719B9F0490013408B /* BANetworkParameters.m */, 02849B4819B9F0490013408B /* BAParameter.h */, 02849B4919B9F0490013408B /* BAParameter.m */, 02849B4A19B9F0490013408B /* BAPropertiesCenter.h */, @@ -1615,6 +1667,7 @@ 02849BDA19B9FA2D0013408B /* Modules */ = { isa = PBXGroup; children = ( + 7E53722E2BBED10F00F2E78A /* Data Collection */, 1EBDBC7C27188154006DC662 /* Metric */, 1EA42E0126A9A5D8004CC618 /* Debug */, 626D8552240032F600C24F19 /* Display Receipt */, @@ -1624,6 +1677,7 @@ 51A214EF25CC6073003FD6A6 /* Local Campaigns */, 51BD1924202074D200C2524D /* Actions */, 02849BDB19B9FA2D0013408B /* Core */, + 51BE8C0F2B8C984C000489BA /* Profile */, 02849BDF19B9FA2D0013408B /* Push */, 02849BE219B9FA2D0013408B /* Tracker */, 514CD7291BFE29F000F79110 /* User */, @@ -1636,7 +1690,6 @@ children = ( 02849BDC19B9FA2D0013408B /* batchConfigurationTests.m */, 02849BDD19B9FA2D0013408B /* batchCoreCenterTests.m */, - 02955F5D19DAE0E800E7458A /* batchUserProfileTests.m */, 02849BDE19B9FA2D0013408B /* batchStatusTests.m */, 514FE7292209F5C0003C3426 /* batchOptOutTests.m */, ); @@ -1649,6 +1702,7 @@ 02849BE019B9FA2D0013408B /* batchPushCenterTests.m */, 02849BE119B9FA2D0013408B /* batchPushMessageTests.m */, 514FFCCD250903F3009D6A20 /* batchUNUserNotificationCenterDelegateTests.m */, + 518F17132BBD7E9C003122CF /* pushAuthorizationTests.swift */, ); path = Push; sourceTree = ""; @@ -1752,20 +1806,23 @@ 0284998119B9EFBA0013408B /* Batch.h */, 51C7D2531B14A1740038FAD7 /* BatchCore.h */, 0284998219B9EFBA0013408B /* BatchCore.m */, - 511F1E1521109D3800796AD9 /* BatchEventData.h */, - 511F1E1621109D3800796AD9 /* BatchEventData.m */, - 511F1E332110AA0F00796AD9 /* BatchEventDataPrivate.h */, - 022AC42A19DAA36900BDC3E7 /* BatchUserProfile.h */, - 022AC42B19DAA36900BDC3E7 /* BatchUserProfile.m */, + 7E01A6422BC5410100B0105B /* BatchDataCollectionConfig.h */, + 7E01A6432BC5410100B0105B /* BatchDataCollectionConfig.m */, + 511F1E1521109D3800796AD9 /* BatchEventAttributes.h */, + 511F1E1621109D3800796AD9 /* BatchEventAttributes.m */, + 511F1E332110AA0F00796AD9 /* BatchEventAttributesPrivate.h */, 6258702D2354C486006B0287 /* BatchEventDispatcher.h */, 6258702E2354C8C1006B0287 /* BatchEventDispatcher.m */, 51F222AB1B4C16AC005DFBCE /* BatchLogger.h */, 518E38411EA8EB93009AB274 /* BatchInbox.h */, 518E38421EA8EB93009AB274 /* BatchInbox.m */, 5142AD891EBCAB1500B94027 /* BatchInboxPrivate.h */, + 51CADDDC2B88D83200D06BDE /* BatchProfile.h */, + 51CADDDD2B88D83200D06BDE /* BatchProfile.m */, + 51F3D0D82B923434007EBEE3 /* BatchProfileEditor.h */, + 51F3D0D92B923434007EBEE3 /* BatchProfileEditor.m */, 0284998619B9EFBA0013408B /* BatchPush.h */, 0284998719B9EFBA0013408B /* BatchPush.m */, - 51741C4D211C427E00AD03EB /* BatchPushPrivate.h */, 5101539D1BEA5318009E8B16 /* BatchUser.h */, 5101539E1BEA5318009E8B16 /* BatchUser.m */, 930896982209E47700B4CCAE /* BatchUserAttribute.h */, @@ -1815,13 +1872,15 @@ 02849BB819B9FA2D0013408B /* batchCoreTests.m */, 516BE208214819160057D937 /* deeplinkDelegateTests.m */, 51D5779C24409F3F00211582 /* DataUtils.swift */, - 519D342A2111F4B300E5D2BE /* batchEventDataTests.swift */, + 519D342A2111F4B300E5D2BE /* batchEventAttributesTests.swift */, 9323C51D2225A1960097645F /* htmlParserTests.m */, 51B4F50525D3FA3B0077E948 /* OperatingSystemVersion+Equatable.swift */, 5192784F25D17B80005C43B4 /* XCTest+BAPromise.swift */, + 519E69F22B923B6A001483C5 /* XCTest+Injection.swift */, + 51F5B2E62BBD58C0006E2790 /* XCTest+Threading.swift */, 515E45AA251358C300B05B57 /* InvocationRecorders.h */, 515E45AB251358C300B05B57 /* InvocationRecorders.m */, - 9373ECDE2211CA5200FA00A3 /* Stubs */, + 9373ECDE2211CA5200FA00A3 /* Mocks */, 02849BDA19B9FA2D0013408B /* Modules */, 02849BF119B9FA2D0013408B /* Response */, 02849BF919B9FA2D0013408B /* Webservices */, @@ -1900,13 +1959,11 @@ 512354B61BFA2472001C5259 /* BAUserDataOperation.m */, 511074691C208F6300BFC215 /* BAUserDataManager.h */, 5110746A1C208F6300BFC215 /* BAUserDataManager.m */, - 512354BB1BFA30EB001C5259 /* BAUserDataEditor.h */, - 512354BC1BFA30EB001C5259 /* BAUserDataEditor.m */, + 512354BB1BFA30EB001C5259 /* BAInstallDataEditor.h */, + 512354BC1BFA30EB001C5259 /* BAInstallDataEditor.m */, 517EAE411C1890540058D288 /* BAUserAttribute.h */, 517EAE421C1890540058D288 /* BAUserAttribute.m */, 51A31D811E27DC8B004CDD65 /* BAUserDataEnums.h */, - 1E104EAD28E6F43D00E998AE /* BAUserEmailSubscription.h */, - 1E104EAE28E6F43D00E998AE /* BAUserEmailSubscription.m */, ); path = User; sourceTree = ""; @@ -2033,7 +2090,6 @@ 51B4F4FF25D3F76F0077E948 /* osHelperTests.swift */, 515A74942510BE2200F05E80 /* swizzlingTests.m */, 515A74962510C17800F05E80 /* swiftSwizzling.swift */, - 1EB69D3D290C0F5A003E1EF7 /* emailTests.swift */, ); path = Helpers; sourceTree = ""; @@ -2082,6 +2138,7 @@ 51E6660A1CE0CB8200F00E63 /* BADelegatedUIAlertController.m */, 512596E11CDCD8C700AC934B /* BABatchMessagingDelegateWrapper.h */, 512596E21CDCD8C700AC934B /* BABatchMessagingDelegateWrapper.m */, + 51A920492B7E083400E939AC /* FeedbackGenerators.swift */, 518D886B2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.h */, 518D886C2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.m */, 51D3B5EB1F59651A001C98B0 /* BAMSGOverlayWindow.h */, @@ -2130,8 +2187,8 @@ 51ACA05C2166193D006FAD04 /* batchUserDiffTests.swift */, 93C78F5E220AE4120082A6F7 /* batchUserTests.m */, 51242703244D8F9700025DA3 /* batchUserDataEditorTests.swift */, - 51E22DC62451CA3C00D02CA8 /* batchUserEditorPublicAPITests.m */, - 1E104EB528EB289400E998AE /* batchUserEmailSubscriptionTests.m */, + 51F3D0D62B91F697007EBEE3 /* mockUserDatasource.swift */, + 51F3D0D42B91F66E007EBEE3 /* userDataManagerTests.swift */, ); name = User; sourceTree = ""; @@ -2297,8 +2354,6 @@ 93F04586224B7E8E0006C41A /* BADateFormatting.m */, 51D3E79C2328EF7100DF82AF /* BAWindowHelper.h */, 51D3E79D2328EF7100DF82AF /* BAWindowHelper.m */, - 1EB69D39290C09B9003E1EF7 /* BAEmailUtils.h */, - 1EB69D3A290C09B9003E1EF7 /* BAEmailUtils.m */, ); path = Helpers; sourceTree = ""; @@ -2345,10 +2400,26 @@ path = Private; sourceTree = ""; }; + 5172DD0A2B88E2510094784D /* Profile */ = { + isa = PBXGroup; + children = ( + 5172DD0B2B88E2620094784D /* BAProfileCenter.swift */, + 5172DD122B88F7B80094784D /* BATProfileDataValidators.swift */, + 510F99292B8E9192009D15DE /* BATEventAttributesValidator.swift */, + 510F99352B8F8544009D15DE /* BATEventAttributesSerializer.swift */, + 51D9DD8A2B95F73D00F96F3D /* BATProfileEditor.swift */, + 51F6CA532B979D0D005C9F29 /* BatchProfileError+Init.swift */, + 51F1E95E2B9A1D5500F90E06 /* BATProfileOperationsSerializer.swift */, + 516D98AA2BC05A0C00345778 /* BATProfileInstallDataCompatibility.swift */, + ); + path = Profile; + sourceTree = ""; + }; 5179E71A22FB23CB00614F9D /* Dependency Injection */ = { isa = PBXGroup; children = ( 5179E71B22FB240100614F9D /* README.md */, + 51CADDD02B88B77C00D06BDE /* BAInjection.swift */, 5179E71C22FC15C000614F9D /* BAInjection.h */, 5179E71D22FC15C000614F9D /* BAInjection.m */, 5179E72022FC2BC900614F9D /* BAInjectable.h */, @@ -2502,6 +2573,22 @@ path = Actions; sourceTree = ""; }; + 51BE8C0F2B8C984C000489BA /* Profile */ = { + isa = PBXGroup; + children = ( + 510737412BC596950033E2E8 /* profileEditorCompatibilityTest.m */, + 1EB69D3D290C0F5A003E1EF7 /* profileEditorValidationTests.swift */, + 51EBCF652B8CF40700CA3C6F /* profileIdentifyTests.swift */, + 51AB57792BAC810700A1E72B /* profileOperationsSerializerTests.swift */, + 7E39F01C2BC96849009FC9E7 /* profileMigrationTests.swift */, + 514B39A72BAA095D006980EB /* eventDataSerializerTests.swift */, + 51AD7F4C2BA4BCF9009F2F85 /* eventDataValidatorTests.swift */, + 51CA52502BB5CB470093E2CA /* TestProfileEditor.swift */, + 7E39F0232BC97133009FC9E7 /* TestProfileCenter.swift */, + ); + path = Profile; + sourceTree = ""; + }; 51C9D1FD1F1D03F3002583E9 /* Outputs */ = { isa = PBXGroup; children = ( @@ -2644,13 +2731,33 @@ path = Inbox; sourceTree = ""; }; - 9373ECDE2211CA5200FA00A3 /* Stubs */ = { + 7E5372292BBC37A400F2E78A /* Data Collection */ = { + isa = PBXGroup; + children = ( + 7E53722A2BBC384D00F2E78A /* BATDataCollectionCenter.swift */, + 7E53722C2BBC38BC00F2E78A /* BATSystemParameter.swift */, + 7E39F0132BC5904E009FC9E7 /* DataCollectionUtils.swift */, + ); + path = "Data Collection"; + sourceTree = ""; + }; + 7E53722E2BBED10F00F2E78A /* Data Collection */ = { + isa = PBXGroup; + children = ( + 7ED8D8442BBED42E00CAF988 /* dataCollectionCenterTests.swift */, + ); + path = "Data Collection"; + sourceTree = ""; + }; + 9373ECDE2211CA5200FA00A3 /* Mocks */ = { isa = PBXGroup; children = ( 9373ECDB2211C9D800FA00A3 /* DeeplinkDelegateStub.h */, 9373ECDC2211C9D800FA00A3 /* DeeplinkDelegateStub.m */, + 51EBCF6A2B8CF5D200CA3C6F /* EventTrackerMock.swift */, + 51EBCF732B8CFD4200CA3C6F /* BatchUserDataEditorMock.swift */, ); - path = Stubs; + path = Mocks; sourceTree = ""; }; AC00043B80924CF59C5D4990 /* Triggers */ = { @@ -2702,8 +2809,7 @@ files = ( 517D71BC22BBC51900E374FB /* Batch.h in Headers */, 517D70DF22BBC4EB00E374FB /* BatchCore.h in Headers */, - 517D70E022BBC4EC00E374FB /* BatchEventData.h in Headers */, - 517D70E122BBC4EC00E374FB /* BatchUserProfile.h in Headers */, + 517D70E022BBC4EC00E374FB /* BatchEventAttributes.h in Headers */, 517D70E222BBC4EC00E374FB /* BatchLogger.h in Headers */, 517D70E322BBC4EC00E374FB /* BatchInbox.h in Headers */, 517D70E422BBC4EC00E374FB /* BatchPush.h in Headers */, @@ -2713,6 +2819,9 @@ 517D70E822BBC4EC00E374FB /* BatchMessagingModels.h in Headers */, 517D70E922BBC4EC00E374FB /* BatchActions.h in Headers */, 621F1EA92355E9FB002B36DA /* BatchEventDispatcher.h in Headers */, + 51F3D0DA2B923434007EBEE3 /* BatchProfileEditor.h in Headers */, + 7E01A6452BC5410100B0105B /* BatchDataCollectionConfig.h in Headers */, + 51CADDDE2B88D83200D06BDE /* BatchProfile.h in Headers */, 1EF1974427673BBE00386DF0 /* BAInjectionRegistrar.h in Headers */, 620F174E24488AFB00D046D5 /* BADisplayReceipt.h in Headers */, 620F174F24488AFB00D046D5 /* BADisplayReceiptCache.h in Headers */, @@ -2826,7 +2935,7 @@ 517D715622BBC50E00E374FB /* BAUserSQLiteDatasource.h in Headers */, 517D715722BBC50E00E374FB /* BAUserDataOperation.h in Headers */, 517D715822BBC50E00E374FB /* BAUserDataManager.h in Headers */, - 517D715922BBC50E00E374FB /* BAUserDataEditor.h in Headers */, + 517D715922BBC50E00E374FB /* BAInstallDataEditor.h in Headers */, 517D715A22BBC50E00E374FB /* BAUserAttribute.h in Headers */, 517D715B22BBC50E00E374FB /* BAUserDataEnums.h in Headers */, 517D715C22BBC50E00E374FB /* BAUserDataServices.h in Headers */, @@ -2862,7 +2971,6 @@ 517D718722BBC50E00E374FB /* BAResponseHelper.h in Headers */, 517D718822BBC50E00E374FB /* BAGETWebserviceClient.h in Headers */, 517D718922BBC50E00E374FB /* BAQueryWebserviceClient.h in Headers */, - 517D718B22BBC50E00E374FB /* BAWebserviceMetrics.h in Headers */, 517D718C22BBC50E00E374FB /* BALogger.h in Headers */, 517D718D22BBC50E00E374FB /* BALoggerProtocol.h in Headers */, 510BD5DB22E0B4D500D8CBA1 /* BAWSQueryStart.h in Headers */, @@ -2892,7 +3000,6 @@ 517D71A622BBC50F00E374FB /* BASystemDateProvider.h in Headers */, 517D71A722BBC50F00E374FB /* BATZAwareDate.h in Headers */, 517D71A822BBC50F00E374FB /* BAUptimeProvider.h in Headers */, - 517D71A922BBC50F00E374FB /* BANetworkParameters.h in Headers */, 510BD5D522E0B4D500D8CBA1 /* BAWSQueryLocalCampaigns.h in Headers */, 517D71AB22BBC50F00E374FB /* BAPropertiesCenter.h in Headers */, 5179E72222FC2BC900614F9D /* BAInjectable.h in Headers */, @@ -2904,10 +3011,9 @@ 517D71B522BBC50F00E374FB /* BAWebserviceClient.h in Headers */, 517D71BA22BBC50F00E374FB /* BAErrorHelper.h in Headers */, 517D71BB22BBC50F00E374FB /* Defined.h in Headers */, - 517D70EA22BBC50000E374FB /* BatchEventDataPrivate.h in Headers */, + 517D70EA22BBC50000E374FB /* BatchEventAttributesPrivate.h in Headers */, 51711AC023D60A5300FF2B9D /* PrivateUmbrellaHeader.h in Headers */, 517D70EB22BBC50000E374FB /* BatchInboxPrivate.h in Headers */, - 517D70EC22BBC50000E374FB /* BatchPushPrivate.h in Headers */, 62F61A982435D79C00652B29 /* BAUserEventBuiltinActions.h in Headers */, 51711AC123D60CBA00FF2B9D /* BAEventDispatcherCenter.h in Headers */, 51B8074924003DF70006882D /* BATMessagePackReader.h in Headers */, @@ -2933,7 +3039,6 @@ 5127299E23FD720E00999505 /* BATMessagePackWriter.h in Headers */, 51696F7A25263BFC00951CA4 /* BAApplicationLifecycle.h in Headers */, 5145C78224C1F0BE004A89FE /* BAConnectionContentType.h in Headers */, - 5190F1E8253495B30014E5BA /* Versions.h in Headers */, 5162B2F42571561900607780 /* BAMSGWebviewViewController.h in Headers */, 51FCC9332577EF9C00FA76C6 /* BAMSGActivityIndicatorView.h in Headers */, AC0001D6427F0B26FC71D20A /* BAQueryWebserviceIdentifiersProviding.h in Headers */, @@ -2956,9 +3061,8 @@ 1E45578527CE1B510048D3A8 /* BALocalCampaignsJITService.h in Headers */, 1E2C31B527CF740E00A4CB6E /* BAMetricRegistry.h in Headers */, 5157117229DAD6EC0002A9CD /* BATUserActivity.h in Headers */, - 1EB69D3B290C09B9003E1EF7 /* BAEmailUtils.h in Headers */, - 1E104EAF28E6F43D00E998AE /* BAUserEmailSubscription.h in Headers */, 1EBDBC662715B2A2006DC662 /* BAMetricProtocol.h in Headers */, + 5190F1E8253495B30014E5BA /* Versions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3031,7 +3135,8 @@ 02F7845719B9EF5A0006FF07 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1500; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1520; ORGANIZATIONNAME = Batch.com; TargetAttributes = { 02849C3219BA00350013408B = { @@ -3048,7 +3153,7 @@ 517D701722BBC34A00E374FB = { CreatedOnToolsVersion = 11.0; DevelopmentTeam = U5K2ETC2Y6; - LastSwiftMigration = 1410; + LastSwiftMigration = 1520; ProvisioningStyle = Automatic; }; }; @@ -3127,7 +3232,7 @@ ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; - shellScript = "# Work around an issue where release builds of Batch on Xcode 15.3 need to have\n# a very high Deployment target to pass validation\n# https://github.com/BatchLabs/Batch-iOS-SDK/issues/35\n# We can't override this when building so we need to patch the plist before it's bundled\n# in the xcframework or signed\nINFO_PLIST_FILE=\"${BUILT_PRODUCTS_DIR}/Batch.framework/Info.plist\"\nif [ -f \"$INFO_PLIST_FILE\" ]; then \n # We are NOT in a catalyst environment: we only want to patch iOS/visionOS builds\n # We also do not want to patch simulator builds\n if [[ ${SDK_NAME} != *\"simulator\"* ]]; then\n echo \"Patching MinimumOSVersion to 100.0\"\n plutil -replace MinimumOSVersion -string 100.0 $INFO_PLIST_FILE\n fi\nfi\n \n"; + shellScript = "# Work around an issue where release builds of Batch on Xcode 15.3 need to have\n# a very high Deployment target to pass validation\n# https://github.com/BatchLabs/Batch-iOS-SDK/issues/35\n# We can't override this when building so we need to patch the plist before it's bundled\n# in the xcframework or signed\nif [[ -z \"${IS_MACCATALYST}\" ]]; then\n # We are NOT in a catalyst environment: we only want to patch iOS/visionOS builds\n # We also do not want to patch simulator builds\n if [[ ${SDK_NAME} != *\"simulator\"* ]]; then\n echo \"Patching MinimumOSVersion to 100.0\"\n plutil -replace MinimumOSVersion -string 100.0 \"${BUILT_PRODUCTS_DIR}/Batch.framework/Info.plist\"\n fi\nfi\n \n"; }; 516BBC0B22BD0D2900742649 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -3143,10 +3248,11 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# workaround for bitcode generation problem with Xcode 7.3\nunset TOOLCHAINS\n\nexport CONFIGURATION=Release\n\n# define output folder environment variable\nARCHIVES_OUTPUTFOLDER=${PROJECT_DIR}/universal/Archives\nDSYMS_OUTPUTFOLDER=${PROJECT_DIR}/universal/dSYMs\nXCFRAMEWORK_OUTPUT=${PROJECT_DIR}/universal/Batch.xcframework\n\n# Create the output folders\nmkdir -p \"${ARCHIVES_OUTPUTFOLDER}\"\nmkdir -p \"${DSYMS_OUTPUTFOLDER}\"\nrm -f \"${DSYMS_OUTPUTFOLDER}/*.dSYM\"\n\n# Step 1. Build Device, Simulator and macOS versions\nxcodebuild archive -scheme Batch -destination \"generic/platform=iOS\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO OTHER_CFLAGS=\"-fembed-bitcode\" ARCHS=\"arm64 arm64e\" VALID_ARCHS=\"arm64 arm64e\"\nxcodebuild archive -scheme Batch -destination \"generic/platform=iOS Simulator\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO OTHER_CFLAGS=\"-fembed-bitcode\"\nxcodebuild archive -scheme Batch -destination \"platform=macOS\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO OTHER_CFLAGS=\"-fembed-bitcode\"\n\n# Step 2. Copy the dSYMs\n\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_iphoneos.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_iphonesimulator.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_macos.framework.dSYM\"\n\n# Step 3. Clean up the frameworks\n\npushd .\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive/Products/Library/Frameworks/Batch.framework/Versions/A/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\n## 3.1: Fix Xcode not creating the \"PrivateHeaders\" folder, which can break Azure Pipelines\n## https://github.com/bamlab/react-native-batch-push/issues/67\nmkdir \"PrivateHeaders\"\n\npopd\n\n# Step 4. Make the xcframework\n# Remove the old xcframework\nrm -rf \"${XCFRAMEWORK_OUTPUT}\"\n\n# Build the xcframework\n\npushd .\ncd \"${PROJECT_DIR}/universal\"\nxcodebuild -create-xcframework \\\n -framework \"Archives/Batch-iphoneos.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-iphonesimulator.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-macos.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -output \"Batch.xcframework\"\n\n# Sign it for distribution\n# (Apple Enterprise Account)\n\nif [ \"$BATCHSDK_SIGN_XCFRAMEWORK\" == \"1\" ]\nthen\n echo \"Signing Batch.xcframework\"\n codesign --timestamp -v --sign \"iPhone Distribution: IMEDIAPP\" \"${XCFRAMEWORK_OUTPUT}\"\nfi\n"; + shellScript = "# workaround for bitcode generation problem with Xcode 7.3\nunset TOOLCHAINS\n\nexport CONFIGURATION=Release\n\n# define output folder environment variable\nARCHIVES_OUTPUTFOLDER=${PROJECT_DIR}/universal/Archives\nDSYMS_OUTPUTFOLDER=${PROJECT_DIR}/universal/dSYMs\nXCFRAMEWORK_OUTPUT=${PROJECT_DIR}/universal/Batch.xcframework\n\n# Create the output folders\nmkdir -p \"${ARCHIVES_OUTPUTFOLDER}\"\nmkdir -p \"${DSYMS_OUTPUTFOLDER}\"\nrm -f \"${DSYMS_OUTPUTFOLDER}/*.dSYM\"\n\n# Step 1. Build Device, Simulator, macOS and visionOS versions\nxcodebuild archive -scheme Batch -destination \"generic/platform=iOS\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO ARCHS=\"arm64 arm64e\" VALID_ARCHS=\"arm64 arm64e\"\nxcodebuild archive -scheme Batch -destination \"generic/platform=iOS Simulator\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO\nxcodebuild archive -scheme Batch -destination \"platform=macOS\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO\n\n# We need a xcconfig to override SDKROOT to fix an issue with visionOS SDK being\n# required to build xros frameworks https://forums.developer.apple.com/forums/thread/743246\n# Setting SDKROOT or even making a new configuration does not work\ncp \"${PROJECT_DIR}/Batch/ReleaseConfig-visionos-workaround-vision.xcconfig\" \"${PROJECT_DIR}/Batch/ReleaseConfig.xcconfig\"\nxcodebuild archive -scheme Batch -destination \"generic/platform=visionOS\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-xros.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO SDKROOT=xros\nxcodebuild archive -scheme Batch -destination \"generic/platform=visionOS Simulator\" -configuration ${CONFIGURATION} -archivePath \"${ARCHIVES_OUTPUTFOLDER}/Batch-xrossimulator.xcarchive\" SKIP_INSTALL=NO ONLY_ACTIVE_ARCH=NO\ncp \"${PROJECT_DIR}/Batch/ReleaseConfig-visionos-workaround-initial.xcconfig\" \"${PROJECT_DIR}/Batch/ReleaseConfig.xcconfig\"\n\n# Step 2. Copy the dSYMs\n\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_iphoneos.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_iphonesimulator.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_macos.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-xros.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_xros.framework.dSYM\"\ncp -a \"${ARCHIVES_OUTPUTFOLDER}/Batch-xrossimulator.xcarchive/dSYMs/Batch.framework.dSYM\" \"${DSYMS_OUTPUTFOLDER}/Batch_xrossimulator.framework.dSYM\"\n\n# Step 3. Clean up the frameworks\n\npushd .\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphoneos.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-iphonesimulator.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-macos.xcarchive/Products/Library/Frameworks/Batch.framework/Versions/A/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-xros.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\ncd \"${ARCHIVES_OUTPUTFOLDER}/Batch-xrossimulator.xcarchive/Products/Library/Frameworks/Batch.framework/\"\nsh ${PROJECT_DIR}/../Tools/Scripts/strip_public_swift_framework.sh\n\n## 3.1: Fix Xcode not creating the \"PrivateHeaders\" folder, which can break Azure Pipelines\n## https://github.com/bamlab/react-native-batch-push/issues/67\nmkdir \"PrivateHeaders\"\n\npopd\n\n# Step 4. Make the xcframework\n# Remove the old xcframework\nrm -rf \"${XCFRAMEWORK_OUTPUT}\"\n\n# Build the xcframework\n\npushd .\ncd \"${PROJECT_DIR}/universal\"\nxcodebuild -create-xcframework \\\n -framework \"Archives/Batch-iphoneos.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-iphonesimulator.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-macos.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-xros.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -framework \"Archives/Batch-xrossimulator.xcarchive/Products/Library/Frameworks/Batch.framework\" \\\n -output \"Batch.xcframework\"\n\n# Sign it for distribution\n# (Apple Enterprise Account)\n\nif [ \"$BATCHSDK_SIGN_XCFRAMEWORK\" == \"1\" ]\nthen\n echo \"Signing Batch.xcframework\"\n codesign --timestamp -v --sign \"iPhone Distribution: IMEDIAPP\" \"${XCFRAMEWORK_OUTPUT}\"\nfi\n"; }; 516C406723D9FD63000C11CF /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 12; files = ( ); @@ -3175,30 +3281,36 @@ 62579A14235DC4680018B26C /* messageEventPayloadTests.m in Sources */, 6295B154243F44520065059E /* inboxFetchWebserviceClientTests.m in Sources */, 51B4F50025D3F76F0077E948 /* osHelperTests.swift in Sources */, + 51F3D0D52B91F66E007EBEE3 /* userDataManagerTests.swift in Sources */, 62579A11235DBF7C0018B26C /* pushEventPayloadTests.m in Sources */, - 1EB69D3E290C0F5A003E1EF7 /* emailTests.swift in Sources */, + 1EB69D3E290C0F5A003E1EF7 /* profileEditorValidationTests.swift in Sources */, 1EBDBC8427188D40006DC662 /* metricTests.m in Sources */, 51ACA05D2166193D006FAD04 /* batchUserDiffTests.swift in Sources */, + 510737422BC596950033E2E8 /* profileEditorCompatibilityTest.m in Sources */, + 519E69F32B923B6A001483C5 /* XCTest+Injection.swift in Sources */, 515A74972510C17800F05E80 /* swiftSwizzling.swift in Sources */, 51EB8A3723A3D97000879584 /* queryTestHelpers.swift in Sources */, 51A214E625CC09D0003FD6A6 /* webviewUtilsTests.swift in Sources */, 51DD79BC25D2BE0F005E8D62 /* webviewBridgeLegacyTests.swift in Sources */, + 51AB577A2BAC810700A1E72B /* profileOperationsSerializerTests.swift in Sources */, 51F1FCFC20DAA0A7006EDE43 /* batchTaskDebouncerTest.swift in Sources */, 5192784A25D1764D005C43B4 /* webviewJavascriptBridgeTests.swift in Sources */, - 51E22DC72451CA3C00D02CA8 /* batchUserEditorPublicAPITests.m in Sources */, 1EA32C6027B524600085D133 /* localCampaignsSQLTrackerTests.m in Sources */, 51C6A47225CA99DF00B07AB6 /* batchTestsBootstrap.m in Sources */, 516D31C22445C40400BF3AB0 /* messagePackWriterTests.swift in Sources */, 51242705244D905000025DA3 /* batchUserDataEditorTests.swift in Sources */, 51D5779D24409F3F00211582 /* DataUtils.swift in Sources */, + 7E39F01D2BC96849009FC9E7 /* profileMigrationTests.swift in Sources */, + 51EBCF662B8CF40700CA3C6F /* profileIdentifyTests.swift in Sources */, 513347902507D7DB005A89AB /* jsonTests.m in Sources */, + 51F5B2E72BBD58C0006E2790 /* XCTest+Threading.swift in Sources */, 51C6B11525CD53E200B531C5 /* localCampaignsCenterTests.m in Sources */, + 51EBCF6B2B8CF5D200CA3C6F /* EventTrackerMock.swift in Sources */, 515CA89023981A7B002CBDAC /* groupActionTest.swift in Sources */, 516BE209214819160057D937 /* deeplinkDelegateTests.m in Sources */, 51ECC06D22D4F8C800F1C5EC /* webserviceAESGCMCryptorTests.m in Sources */, 1EA42E0426A9A736004CC618 /* FindMyInstallationHelperTests.m in Sources */, 1EBDBC86271D9936006DC662 /* metricManagerTests.m in Sources */, - 1E104EB628EB289400E998AE /* batchUserEmailSubscriptionTests.m in Sources */, 628C158F24AB8E990026C4EC /* inboxSyncWebserviceClientTests.m in Sources */, 5192785025D17B80005C43B4 /* XCTest+BAPromise.swift in Sources */, 5159B4ED25E01BA9008082FA /* loggerSetupTests.m in Sources */, @@ -3210,6 +3322,7 @@ 518D887023560C79007BF341 /* messagingAnalyticsDeduplicatingDelegateTests.swift in Sources */, 514BA4A027D257F100B93FAF /* inboxFetcherTests.swift in Sources */, 939E058A222D2CC700E8D32D /* batchUserTests.m in Sources */, + 51EBCF742B8CFD4200CA3C6F /* BatchUserDataEditorMock.swift in Sources */, 510D906F24C83A81005981AC /* shaTests.swift in Sources */, 1EC803AB27D2477E00CCE8E5 /* localCampaignsManagerTests.swift in Sources */, 51DD799E25D29FA6005E8D62 /* webviewBridgeWKHandlerTests.swift in Sources */, @@ -3220,18 +3333,19 @@ 02849C1A19B9FA2D0013408B /* batchPushMessageTests.m in Sources */, 029B335B1A39A1F7006B9DC6 /* batchAESTests.m in Sources */, 51D5779B24409EE000211582 /* messagePackReaderTests.swift in Sources */, + 7E39F0242BC97133009FC9E7 /* TestProfileCenter.swift in Sources */, 02849C2E19B9FA2D0013408B /* batchResponseHelperTests.m in Sources */, 1EA32C5E27B3A4140085D133 /* localCampaignsTrackerTests.swift in Sources */, 513DA84522F87D6A00144521 /* eventTrackerServiceTests.swift in Sources */, + 51CA52512BB5CB470093E2CA /* TestProfileEditor.swift in Sources */, 62A4811424003D0600A2606D /* displayReceiptTests.m in Sources */, 9373ECDD2211C9D800FA00A3 /* DeeplinkDelegateStub.m in Sources */, 5122032622CA5C0B001A505F /* cssTests.swift in Sources */, 029B335F1A39A1F7006B9DC6 /* batchPropertiesCenterTests.m in Sources */, 02849C1B19B9FA2D0013408B /* batchEventSQLiteDatasourceTests.m in Sources */, - 519D342C2111F4BF00E5D2BE /* batchEventDataTests.swift in Sources */, + 519D342C2111F4BF00E5D2BE /* batchEventAttributesTests.swift in Sources */, 62DEADC42456E00C0065301F /* displayReceiptCacheTests.m in Sources */, 627278E124A491CB0067F036 /* inboxNotificationDatasourceTests.m in Sources */, - 02955F5E19DAE0E800E7458A /* batchUserProfileTests.m in Sources */, 51B4F50625D3FA3B0077E948 /* OperatingSystemVersion+Equatable.swift in Sources */, 5161B0A824C71195003F36E7 /* webserviceHMACTests.swift in Sources */, 9323C51E2225A1960097645F /* htmlParserTests.m in Sources */, @@ -3240,13 +3354,18 @@ 512EA4E523BE37F300B7DBB1 /* pushTokenServiceTests.swift in Sources */, 02849C1919B9FA2D0013408B /* batchPushCenterTests.m in Sources */, 514FE72A2209F5C0003C3426 /* batchOptOutTests.m in Sources */, + 7ED8D8452BBED42E00CAF988 /* dataCollectionCenterTests.swift in Sources */, 029B33601A39A1F7006B9DC6 /* batchUserDefaultsTests.m in Sources */, + 51AD7F4D2BA4BCF9009F2F85 /* eventDataValidatorTests.swift in Sources */, 51BD19262020759A00C2524D /* builtinActionsTest.m in Sources */, 519BC8EE22D5B98100C3BBF7 /* webserviceCryptorFactoryTests.m in Sources */, 5159B4DF25E0044C008082FA /* internalLoggerTests.swift in Sources */, 02849BFC19B9FA2D0013408B /* batchErrorTests.m in Sources */, 514FFCCE250903F3009D6A20 /* batchUNUserNotificationCenterDelegateTests.m in Sources */, + 51F3D0D72B91F697007EBEE3 /* mockUserDatasource.swift in Sources */, 515A74952510BE2200F05E80 /* swizzlingTests.m in Sources */, + 518F17142BBD7E9C003122CF /* pushAuthorizationTests.swift in Sources */, + 514B39A82BAA095D006980EB /* eventDataSerializerTests.swift in Sources */, 02849C1619B9FA2D0013408B /* batchConfigurationTests.m in Sources */, 029B335A1A39A1F7006B9DC6 /* batchNSURLREquest+BAHeadersTests.m in Sources */, 51DD79C225D2C5BE005E8D62 /* webviewTestHelpers.swift in Sources */, @@ -3343,14 +3462,14 @@ 1EBDBC5F27146B8B006DC662 /* BAMetric.m in Sources */, 517D702022BBC45900E374FB /* BatchCore.m in Sources */, 5145C78B24C1FFC6004A89FE /* BATWebserviceHMAC.m in Sources */, - 517D702122BBC45900E374FB /* BatchEventData.m in Sources */, + 517D702122BBC45900E374FB /* BatchEventAttributes.m in Sources */, 51D3E79F2328EF7100DF82AF /* BAWindowHelper.m in Sources */, - 517D702222BBC45900E374FB /* BatchUserProfile.m in Sources */, 517D702322BBC45900E374FB /* BatchInbox.m in Sources */, 517D702422BBC45900E374FB /* BatchPush.m in Sources */, 5157117329DAD6EC0002A9CD /* BATUserActivity.m in Sources */, 517D702522BBC45900E374FB /* BatchUser.m in Sources */, 517CC28825B6DAC200A3F380 /* BATWebviewBridgeLegacyWKHandler.m in Sources */, + 7E01A6442BC5410100B0105B /* BatchDataCollectionConfig.m in Sources */, 517D702622BBC45900E374FB /* BatchUserAttribute.m in Sources */, 5145C78F24C587EA004A89FE /* BASHA.m in Sources */, 51FCC9342577EF9C00FA76C6 /* BAMSGActivityIndicatorView.m in Sources */, @@ -3391,6 +3510,7 @@ 517D704722BBC45900E374FB /* BANewSessionSignal.m in Sources */, 517D704822BBC45900E374FB /* BALocalCampaignsSQLTracker.m in Sources */, 517D704922BBC45900E374FB /* BALocalCampaignCountedEvent.m in Sources */, + 5172DD112B88EE230094784D /* BATInternalEvents.swift in Sources */, 517D704A22BBC45900E374FB /* BALocalCampaignLandingOutput.m in Sources */, 516A3B8F22FC4A6400E6A2FD /* BAInjectionRegistry.m in Sources */, 517D704B22BBC45900E374FB /* BAPushPayload.m in Sources */, @@ -3401,17 +3521,19 @@ 517D705322BBC45900E374FB /* BAEventSQLiteDatasource.m in Sources */, 5184DE092433882E003F2DBB /* msgpack-c.c in Sources */, 517D705422BBC45900E374FB /* BAEventSQLiteHelper.m in Sources */, + 510F99282B8E36EE009D15DE /* BATRegularExpression.swift in Sources */, 517D705522BBC45900E374FB /* BATrackerCenter.m in Sources */, 621F1EC723561BD6002B36DA /* BAMessageEventPayload.m in Sources */, 517D705622BBC45900E374FB /* BATrackerScheduler.m in Sources */, 517D705722BBC45900E374FB /* BATrackerSender.m in Sources */, 517D705822BBC45900E374FB /* BATrackerSignpostHelper.m in Sources */, 517D705B22BBC45900E374FB /* BAMessagingCenter.m in Sources */, - 1E104EB028E6F43D00E998AE /* BAUserEmailSubscription.m in Sources */, 51379B8022FC5C0200AB7B82 /* BAInjectableImplementations.m in Sources */, 517D705C22BBC45900E374FB /* BADelegatedUIAlertController.m in Sources */, 517D705D22BBC45900E374FB /* BABatchMessagingDelegateWrapper.m in Sources */, 510BD5D822E0B4D500D8CBA1 /* BAWSQueryAttributes.m in Sources */, + 51F1E95F2B9A1D5500F90E06 /* BATProfileOperationsSerializer.swift in Sources */, + 51F6CA542B979D0D005C9F29 /* BatchProfileError+Init.swift in Sources */, 517D705E22BBC45900E374FB /* BAMSGOverlayWindow.m in Sources */, 517D705F22BBC45900E374FB /* BAMSGImageDownloader.m in Sources */, 517D706022BBC45900E374FB /* BAMSGMessage.m in Sources */, @@ -3419,6 +3541,7 @@ 517D706222BBC45900E374FB /* BAMSGCTA.m in Sources */, 5184045B22E9F5AF006322DF /* BALocalCampaignsService.m in Sources */, 517D706322BBC45900E374FB /* BAMSGPayloadParser.m in Sources */, + 51CADDD12B88B77C00D06BDE /* BAInjection.swift in Sources */, 517D706422BBC45900E374FB /* BATHtmlParser.m in Sources */, 518D886E2355F653007BF341 /* BAMessagingAnalyticsDeduplicatingDelegate.m in Sources */, 517D706522BBC45900E374FB /* BACSS.m in Sources */, @@ -3447,6 +3570,7 @@ 517D707822BBC45900E374FB /* BAMSGImageView.m in Sources */, 1EBDBC6E2716C94F006DC662 /* BAMetricWebserviceClient.m in Sources */, 517D707922BBC45900E374FB /* BAMSGRemoteImageView.m in Sources */, + 510F992A2B8E9192009D15DE /* BATEventAttributesValidator.swift in Sources */, 517D707A22BBC45900E374FB /* BAMSGLabel.m in Sources */, 517D707B22BBC45900E374FB /* BAMSGStylableView.m in Sources */, 517D707C22BBC45900E374FB /* BAMSGGradientView.m in Sources */, @@ -3460,19 +3584,21 @@ 517D708122BBC45900E374FB /* BAUserCenter.m in Sources */, 5158BB7D22F1EFF200ED0C6C /* BAWebserviceClientExecutor.m in Sources */, 624510E524A0EF0E001A37D4 /* BAInboxWebserviceResponse.m in Sources */, + 51CADDDF2B88D83200D06BDE /* BatchProfile.m in Sources */, 517D708222BBC45900E374FB /* BAUserDataDiff.m in Sources */, 517D708322BBC45900E374FB /* BAUserSQLiteDatasource.m in Sources */, + 7E53722D2BBC38BC00F2E78A /* BATSystemParameter.swift in Sources */, 517D708422BBC45900E374FB /* BAUserDataOperation.m in Sources */, 517D708522BBC45900E374FB /* BAUserDataManager.m in Sources */, - 517D708622BBC45900E374FB /* BAUserDataEditor.m in Sources */, + 517D708622BBC45900E374FB /* BAInstallDataEditor.m in Sources */, 517D708722BBC45900E374FB /* BAUserAttribute.m in Sources */, 517D708822BBC45900E374FB /* BAUserDataServices.m in Sources */, 1E45578627CE1B510048D3A8 /* BALocalCampaignsJITService.m in Sources */, + 7E53722B2BBC384D00F2E78A /* BATDataCollectionCenter.swift in Sources */, 517D708922BBC45900E374FB /* BAActionsCenter.m in Sources */, 629B42A52490D8FA0025411E /* BATGZIP.m in Sources */, 629B42A12490C9120025411E /* BAWebserviceAESGCMGzipCryptor.m in Sources */, 517D708A22BBC45900E374FB /* BAUserDataBuiltinActions.m in Sources */, - 1EB69D3C290C09B9003E1EF7 /* BAEmailUtils.m in Sources */, 517D708B22BBC45900E374FB /* BAInbox.m in Sources */, 517D708C22BBC45900E374FB /* BAInboxFetchWebserviceClient.m in Sources */, 6231A5252458171100AFFA3D /* BAWebserviceJsonClient.m in Sources */, @@ -3482,6 +3608,8 @@ 517D708F22BBC45900E374FB /* BAOptOutWebserviceClient.m in Sources */, 51696F7B25263BFC00951CA4 /* BAApplicationLifecycle.m in Sources */, 517D709022BBC45900E374FB /* BADBGModule.m in Sources */, + 51D9DD8B2B95F73D00F96F3D /* BATProfileEditor.swift in Sources */, + 510F99362B8F8544009D15DE /* BATEventAttributesSerializer.swift in Sources */, 517D709122BBC45900E374FB /* BADBGNameValueListItem.m in Sources */, 517D709222BBC45900E374FB /* BADBGDebugViewController.m in Sources */, 517D709322BBC45900E374FB /* BADBGIdentifiersViewController.m in Sources */, @@ -3509,12 +3637,13 @@ 517D70B222BBC45900E374FB /* BAGETWebserviceClient.m in Sources */, 624510DF24A09B9F001A37D4 /* BAInboxSQLiteHelper.m in Sources */, 517D70B322BBC45900E374FB /* BAQueryWebserviceClient.m in Sources */, - 517D70B522BBC45900E374FB /* BAWebserviceMetrics.m in Sources */, 510BD5C822E0B4BA00D8CBA1 /* BAWSResponseLocalCampaigns.m in Sources */, 517D70B622BBC45900E374FB /* BALogger.m in Sources */, + 516D98AB2BC05A0C00345778 /* BATProfileInstallDataCompatibility.swift in Sources */, 517D70B822BBC45900E374FB /* BALoggerUnified.m in Sources */, 517D70B922BBC45900E374FB /* BANullHelper.m in Sources */, 517D70BA22BBC45900E374FB /* BAThreading.m in Sources */, + 7E39F0142BC5904E009FC9E7 /* DataCollectionUtils.swift in Sources */, 517D70BB22BBC45900E374FB /* BADelegatedApplicationDelegate.m in Sources */, 517D70BC22BBC45900E374FB /* BAJson.m in Sources */, 517D70BD22BBC45900E374FB /* BATJsonDictionary.m in Sources */, @@ -3528,20 +3657,24 @@ 517D70C422BBC45900E374FB /* BAPromise.m in Sources */, 517D70C522BBC45900E374FB /* BAConcurrentQueue.m in Sources */, 517D70C622BBC45900E374FB /* BATaskDebouncer.m in Sources */, + 51CADDCF2B88AD3B00D06BDE /* BATEventTrackerProtocol.swift in Sources */, 517D70C722BBC45900E374FB /* BAAESB64Cryptor.m in Sources */, 511B921022CE44F9002BC4A8 /* BAWebserviceAESGCMCryptor.m in Sources */, 517D70CA22BBC45900E374FB /* BAMultiDelegatesProxy.m in Sources */, 51512E1925B1996B005D85C3 /* BATWebviewJavascriptBridge.m in Sources */, + 51A9204A2B7E083400E939AC /* FeedbackGenerators.swift in Sources */, 517D70CB22BBC45900E374FB /* BASecureDateProvider.m in Sources */, 517D70CC22BBC45900E374FB /* BASystemDateProvider.m in Sources */, 517CC29425B6E02A00A3F380 /* BATWebviewBridgeWKHandler.m in Sources */, + 510F99262B8E2F35009D15DE /* BATSDKError.swift in Sources */, 517D70CD22BBC45900E374FB /* BATZAwareDate.m in Sources */, 510BD5CA22E0B4BA00D8CBA1 /* BAWSResponseStart.m in Sources */, + 51F3D0DB2B923434007EBEE3 /* BatchProfileEditor.m in Sources */, 517D70CE22BBC45900E374FB /* BAUptimeProvider.m in Sources */, - 517D70CF22BBC45900E374FB /* BANetworkParameters.m in Sources */, 517D70D022BBC45900E374FB /* BAParameter.m in Sources */, 5184045122E9F27D006322DF /* BAEventTrackerService.m in Sources */, 517D70D122BBC45900E374FB /* BAPropertiesCenter.m in Sources */, + 5172DD0C2B88E2620094784D /* BAProfileCenter.swift in Sources */, 517D70D222BBC45900E374FB /* BAUserDefaults.m in Sources */, 517D70D322BBC45900E374FB /* BAReachability.m in Sources */, 1EF1974527673BBE00386DF0 /* BAInjectionRegistrar.m in Sources */, @@ -3555,6 +3688,7 @@ 517D70DC22BBC45900E374FB /* BASecureDate.m in Sources */, 62DEADCE24570BC60065301F /* BADisplayReceiptWebserviceClient.m in Sources */, 517D70DD22BBC45900E374FB /* BAErrorHelper.m in Sources */, + 5172DD132B88F7B80094784D /* BATProfileDataValidators.swift in Sources */, 5171445C22DCA29D001E99D7 /* BAStandardQueryWebserviceIdentifiersProvider.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3626,13 +3760,15 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos xrsimulator xros"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; @@ -3679,14 +3815,16 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos xrsimulator xros"; SWIFT_COMPILATION_MODE = wholemodule; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; @@ -3703,7 +3841,7 @@ ); HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Batch/**"; INFOPLIST_FILE = batchTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3735,7 +3873,7 @@ GCC_PREFIX_HEADER = "$(SOURCE_ROOT)/Batch/batch-Prefix.pch"; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Batch/**"; INFOPLIST_FILE = batchTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3760,12 +3898,16 @@ 516BBC0E22BD0D2900742649 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos xrsimulator xros"; }; name = Debug; }; 516BBC0F22BD0D2900742649 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos xrsimulator xros"; }; name = Release; }; @@ -3843,13 +3985,13 @@ INFOPLIST_PREFIX_HEADER = Batch/Versions.h; INFOPLIST_PREPROCESS = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MACH_O_TYPE = staticlib; + MACH_O_TYPE = mh_dylib; + MERGEABLE_LIBRARY = YES; MODULEMAP_FILE = Batch/Batch.modulemap; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; @@ -3857,10 +3999,13 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.batch.Batch; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.7; - TARGETED_DEVICE_FAMILY = "1,2"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; @@ -3871,6 +4016,10 @@ "$(ARCHS_STANDARD)", arm64e, ); + "ARCHS[sdk=xros*]" = ( + "$(ARCHS_STANDARD)", + arm64e, + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -3898,22 +4047,25 @@ INFOPLIST_PREFIX_HEADER = Batch/Versions.h; INFOPLIST_PREPROCESS = YES; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MACH_O_TYPE = staticlib; + MACH_O_TYPE = mh_dylib; + MERGEABLE_LIBRARY = YES; MODULEMAP_FILE = Batch/Batch.modulemap; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.batch.Batch; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; SKIP_INSTALL = YES; - SWIFT_VERSION = 5.7; - TARGETED_DEVICE_FAMILY = "1,2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; diff --git a/Sources/Batch.xcodeproj/xcshareddata/xcschemes/Batch.xcscheme b/Sources/Batch.xcodeproj/xcshareddata/xcschemes/Batch.xcscheme index 36a487e..f3aa2a1 100644 --- a/Sources/Batch.xcodeproj/xcshareddata/xcschemes/Batch.xcscheme +++ b/Sources/Batch.xcodeproj/xcshareddata/xcschemes/Batch.xcscheme @@ -1,6 +1,6 @@ #import -#import +#import +#import #import #import #import #import #import +#import +#import #import #import #import diff --git a/Sources/Batch/Batch.modulemap b/Sources/Batch/Batch.modulemap index ef8dad7..c8f23a3 100644 --- a/Sources/Batch/Batch.modulemap +++ b/Sources/Batch/Batch.modulemap @@ -7,7 +7,14 @@ framework module Batch { explicit module Core { header "BatchCore.h" - header "BatchUserProfile.h" + header "BatchDataCollectionConfig.h" + export * + } + + explicit module Profile { + header "BatchProfile.h" + header "BatchProfileEditor.h" + header "BatchEventAttributes.h" export * } @@ -39,7 +46,6 @@ framework module Batch { explicit module User { header "BatchUser.h" - header "BatchEventData.h" header "BatchUserAttribute.h" export * } diff --git a/Sources/Batch/BatchActions.h b/Sources/Batch/BatchActions.h index eb4c766..187200c 100644 --- a/Sources/Batch/BatchActions.h +++ b/Sources/Batch/BatchActions.h @@ -90,7 +90,7 @@ typedef void (^BatchUserActionBlock)(NSString *_Nonnull identifier, @end /// BatchActions error code constants. -enum { +typedef NS_ENUM(NSInteger, BatchActionError) { /// Internal error BatchActionErrorUnknown = -1001, @@ -101,6 +101,3 @@ enum { /// This action identifier is reserved and cannot be used. Note that actions cannot begin by "batch." BatchActionErrorReservedIdentifier = -1003 }; - -/// @typedef BatchActionError -typedef NSInteger BatchActionError; diff --git a/Sources/Batch/BatchCore.h b/Sources/Batch/BatchCore.h index 5f8e2d3..87c1522 100644 --- a/Sources/Batch/BatchCore.h +++ b/Sources/Batch/BatchCore.h @@ -6,8 +6,9 @@ // Copyright (c) Batch SDK. All rights reserved. // +#import #import -#import + #import #import @@ -22,8 +23,23 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { BatchOptOutNetworkErrorPolicyCancel, }; +/// Batch migrations types +typedef NS_OPTIONS(NSUInteger, BatchMigration) { + /// No migrations disabled + BatchMigrationNone = 0, + + /// Whether Bath should automatically identify logged-in user when running the SDK v2 for the first time. + /// This mean user with a custom_user_id will be automatically attached a to a Profile and can be targeted within a + /// Project scope. + BatchMigrationCustomID = 1 << 0, + + /// Whether Bath should automatically attach current installation's data (language/region/customDataAttributes...) + /// to the User's Profile when running the SDK v2 for the first time. + BatchMigrationCustomData = 1 << 1, +}; + /// Batch's main entry point. -@interface Batch : NSObject +@interface BatchSDK : NSObject /// Use the deeplink delegate object to process deeplink open requests from Batch. /// @@ -55,50 +71,12 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// - key: Your APP's API Key, LIVE or DEV. You can find it on your dashboard. + (void)startWithAPIKey:(NSString *_Nonnull)key; -/// Handles an URL, if applicable. -/// -/// Call this method in `application:openURL:sourceApplication:annotation:` of your `UIApplicationDelegate` -/// - Parameters: -/// - url: The URL given to you by iOS -/// - Returns: YES if Batch performed an action with this URL, NO otherwise. -+ (BOOL)handleURL:(NSURL *_Nonnull)url __attribute__((warn_unused_result))NS_AVAILABLE_IOS(8_0); - -/// Check if Batch is running in development mosde. -/// -/// - Returns: YES if Batch is started __AND__ if it uses a development API key. -+ (BOOL)isRunningInDevelopmentMode __attribute__((warn_unused_result))NS_AVAILABLE_IOS(8_0); - -/// Access the default user profile object. -/// -/// - Deprecated: Please use Batch User instead -/// - Returns: An instance of ``BatchUserProfile``, or nil -+ (BatchUserProfile *_Nullable)defaultUserProfile - __attribute__((warn_unused_result, deprecated("Please use Batch User instead")))NS_AVAILABLE_IOS(8_0); - -/// As Batch has removed support for automatic IDFA collection, this method does nothing. The SDK will not collect the -/// IDFA from the system. -/// - Warning: This method has been deprecated -/// - Parameters: -/// - use: This parameter doesn't do anything -+ (void)setUseIDFA:(BOOL)use __attribute__((deprecated)); - -/// Set if Batch can use advanced device identifiers (default = YES) -/// -/// Advanced device identifiers include information about the device itself, but nothing that directly identify the -/// user, such as but not limited to: -/// - Device model -/// - Device brand -/// - Carrier name -/// - Parameters: -/// - use: YES if Batch can try to use advanced device information, NO if you don't -+ (void)setUseAdvancedDeviceInformation:(BOOL)use; - /// Set if Batch should send its logs to a custom object of yours. /// /// - Important: Be careful with your implementation: setting this can impact stability and performance. You should only /// use it if you know what you are doing. /// - Parameter loggerDelegate: An object implementing ``Batch/BatchLoggerDelegate``. Weakly retained. -+ (void)setLoggerDelegate:(id _Nullable)loggerDelegate; +@property (class, nullable) id loggerDelegate; /// Get the debug view controller. /// @@ -106,7 +84,7 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// implementation more easily. If you want to make it accessible in production, you should hide it in a hard to /// reproduce sequence. /// - Note: Should be presented modally. -+ (UIViewController *_Nullable)debugViewController; ++ (UIViewController *_Nullable)makeDebugViewController; /// Toogle whether internal logs should be logged or not. /// @@ -117,20 +95,33 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// - Parameter enableInternalLogs: Whether to enable development logs. Default: false (unless enabled via CLI). + (void)setInternalLogsEnabled:(BOOL)enableInternalLogs; +/// Configure the SDK Automatic Data Collection. +/// +/// - Parameter editor: A block that will be called with an instance of the automatic data collection configuration as a +/// parameter. Modify the instance of the config to fine-tune the data you authorize to be tracked by Batch. +/// - Note: Batch will persist the changes, so you can call this method at any time according to user consent. +/// ```swift +/// Batch.updateAutomaticDataCollection { config in +/// config.setGeoIPEnabled(false) // Deny Batch from resolving the user's region from the ip address. +/// config.setDeviceModelEnabled(true) // Authorize Batch to use the user's device model information. +/// } +/// ``` ++ (void)updateAutomaticDataCollection:(_Nonnull BatchDataCollectionConfigEditor)editor; + /// Opt-out from Batch SDK usage. /// /// - Important: Calling this method when Batch hasn't started does nothing: Please call -/// ``Batch/Batch/startWithAPIKey:`` beforehand. +/// ``Batch/BatchSDK/startWithAPIKey:`` beforehand. /// /// A push opt-out command will be sent to Batch's servers if the user is connected to the internet. /// If disconnected, notifications might not be disabled properly. Please use -/// ``Batch/Batch/optOutWithCompletionHandler:`` to handle these cases more gracefully. +/// ``Batch/BatchSDK/optOutWithCompletionHandler:`` to handle these cases more gracefully. /// /// Your app should be prepared to handle these cases. Some modules might behave unexpectedly when the SDK is opted out /// from. /// /// Opting out will: -/// - Prevent ``Batch/Batch/startWithAPIKey:`` from starting the SDK +/// - Prevent ``Batch/BatchSDK/startWithAPIKey:`` from starting the SDK /// - Disable any network capability from the SDK /// - Disable all In-App campaigns /// - Make the Inbox module return an error immediatly @@ -140,30 +131,30 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// Even if you opt-in afterwards, data generated (such as user data or tracked events) while opted out __WILL__ be /// lost. /// -/// If you also want to delete user data, please see ``Batch/Batch/optOutAndWipeData``. +/// If you also want to delete user data, please see ``Batch/BatchSDK/optOutAndWipeData``. + (void)optOut; /// Opt-out from Batch SDK and wipe data. /// /// - Important: Calling this method when Batch hasn't started does nothing: Please call -/// ``Batch/Batch/startWithAPIKey:`` beforehand test. +/// ``Batch/BatchSDK/startWithAPIKey:`` beforehand test. /// /// An installation data wipe command will be sent to Batch's servers if the user is connected to the internet. /// If disconnected, notifications might not be disabled properly. Please use -/// ``Batch/Batch/optOutAndWipeDataWithCompletionHandler:`` to handle these cases more gracefully. +/// ``Batch/BatchSDK/optOutAndWipeDataWithCompletionHandler:`` to handle these cases more gracefully. /// -/// See ``Batch/Batch/optOut`` documentation for details. +/// See ``Batch/BatchSDK/optOut`` documentation for details. /// -/// - Note: Once opted out, ``Batch/Batch/startWithAPIKey:`` will essentially be a no-op. Your app should be prepared to -/// handle these cases. +/// - Note: Once opted out, ``Batch/BatchSDK/startWithAPIKey:`` will essentially be a no-op. Your app should be prepared +/// to handle these cases. + (void)optOutAndWipeData; /// Opt-out from Batch SDK. /// /// - Important: Calling this method when Batch hasn't started does nothing: Please call -/// ``Batch/Batch/startWithAPIKey:`` beforehand test. +/// ``Batch/BatchSDK/startWithAPIKey:`` beforehand test. /// -/// See ``Batch/Batch/optOut`` documentation for details. +/// See ``Batch/BatchSDK/optOut`` documentation for details. /// /// Use the completion handler to be informed about whether the opt-out request has been successfully sent to the server /// or not. You'll also be able to control what to do in case of failure. @@ -177,9 +168,9 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// Opt-out from Batch SDK and wipe data. /// /// - Important: Calling this method when Batch hasn't started does nothing: Please call -/// ``Batch/Batch/startWithAPIKey:`` beforehand test. +/// ``Batch/BatchSDK/startWithAPIKey:`` beforehand test. /// -/// See ``Batch/Batch/optOut`` documentation for details. +/// See ``Batch/BatchSDK/optOut`` documentation for details. /// /// Use the completion handler to be informed about whether the opt-out request has been successfully sent to the server /// or not. You'll also be able to control what to do in case of failure. @@ -192,13 +183,13 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// Opt-in to Batch SDK. /// -/// Useful if you called ``Batch/Batch/optOut``, ``Batch/Batch/optOutAndWipeData`` or opted out by default in your +/// Useful if you called ``Batch/BatchSDK/optOut``, ``Batch/BatchSDK/optOutAndWipeData`` or opted out by default in your /// Info.plist. -/// - Important: You will need to call ``Batch/Batch/startWithAPIKey:`` after this. +/// - Important: You will need to call ``Batch/BatchSDK/startWithAPIKey:`` after this. + (void)optIn; /// Returns whether Batch has been opted out from or not -+ (BOOL)isOptedOut; +@property (readonly, class) BOOL isOptedOut; /// Set your list of associated domains, If your app handle universal links. /// @@ -208,13 +199,31 @@ typedef NS_ENUM(NSUInteger, BatchOptOutNetworkErrorPolicy) { /// - Important: Make sure to only include the desired subdomain and the top-level domain. Don’t include path and query /// components or a trailing slash (/). /// - Parameter domains: An array of your supported associated domains. -+ (void)setAssociatedDomains:(NSArray *_Nonnull)domains; +@property (class, nonnull) NSArray *associatedDomains; + +/// Set data migrations you want to disable. +/// +/// - Important: Make sure to call this method before ``Batch/BatchSDK/startWithAPIKey:``. +/// - Parameter migrations: migrations to disable +/// +/// ## Examples: +/// ```swift +/// /// Swift +/// /// Disabling custom ID and Data migrations +/// BatchSDK.setDisabledMigrations([.customID, .customData]) +/// ``` +/// ```objc +/// /// Objective-C +/// /// Disabling custom ID and Data migrations +/// [BatchSDK setDisabledMigrations: BatchMigrationCustomID | BatchMigrationCustomData]; +/// ``` ++ (void)setDisabledMigrations:(BatchMigration)migrations; @end /// BatchDeeplinkDelegate is the protocol to adopt when you want to set a deeplink delegate on the SDK. /// -/// See ``Batch/Batch/deeplinkDelegate`` for more info. +/// See ``Batch/BatchSDK/deeplinkDelegate`` for more info. @protocol BatchDeeplinkDelegate /// Method called when Batch needs to open a deeplink. diff --git a/Sources/Batch/BatchCore.m b/Sources/Batch/BatchCore.m index ee3513b..83ede40 100644 --- a/Sources/Batch/BatchCore.m +++ b/Sources/Batch/BatchCore.m @@ -7,7 +7,6 @@ // #import -#import #import #import @@ -15,34 +14,15 @@ #import #import #import +#import -@implementation Batch +@implementation BatchSDK // Activate the whole Batch system. + (void)startWithAPIKey:(NSString *)key { [BACenterMulticastDelegate startWithAPIKey:key]; } -// Give the URL to Batch systems. -+ (BOOL)handleURL:(NSURL *)url { - return [BACenterMulticastDelegate handleURL:url]; -} - -// Test if Batch is running in development mode. -+ (BOOL)isRunningInDevelopmentMode { - return [BACoreCenter isRunningInDevelopmentMode]; -} - -+ (BatchUserProfile *)defaultUserProfile { - static BatchUserProfile *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[BatchUserProfile alloc] init]; - }); - - return sharedInstance; -} - // Set if Batch can try to use IDFA. Deprecated. + (void)setUseIDFA:(BOOL)use { [BALogger publicForDomain:nil message:@"Ignoring 'setUseIDFA' API call: Batch has removed support for IDFA."]; @@ -52,11 +32,11 @@ + (void)setLoggerDelegate:(id)loggerDelegate { [[[BACoreCenter instance] configuration] setLoggerDelegate:loggerDelegate]; } -+ (void)setUseAdvancedDeviceInformation:(BOOL)use { - [BACenterMulticastDelegate setUseAdvancedDeviceInformation:use]; ++ (id)loggerDelegate { + return [[[BACoreCenter instance] configuration] loggerDelegate]; } -+ (UIViewController *)debugViewController { ++ (UIViewController *)makeDebugViewController { return [BADBGModule debugViewController]; } @@ -100,6 +80,10 @@ + (BOOL)isOptedOut { return [[BAOptOut instance] isOptedOut]; } ++ (NSArray *_Nonnull)associatedDomains { + return [[[[BACoreCenter instance] configuration] associatedDomains] copy]; +} + + (void)setAssociatedDomains:(NSArray *_Nonnull)domains { [[[BACoreCenter instance] configuration] setAssociatedDomains:domains]; } @@ -120,4 +104,12 @@ + (void)setEnablesFindMyInstallation:(BOOL)enablesFindMyInstallation { [BADBGFindMyInstallationHelper setEnablesFindMyInstallation:enablesFindMyInstallation]; } ++ (void)updateAutomaticDataCollection:(_Nonnull BatchDataCollectionConfigEditor)editor { + [[BATDataCollectionCenter sharedInstance] updateDataCollectionConfigWithEditor:editor]; +} + ++ (void)setDisabledMigrations:(BatchMigration)migrations { + [[[BACoreCenter instance] configuration] setDisabledMigrations:migrations]; +} + @end diff --git a/Sources/Batch/BatchDataCollectionConfig.h b/Sources/Batch/BatchDataCollectionConfig.h new file mode 100644 index 0000000..cbba7c6 --- /dev/null +++ b/Sources/Batch/BatchDataCollectionConfig.h @@ -0,0 +1,39 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Object holding the configuration parameters for the automatic data collect. +@interface BatchDataCollectionConfig : NSObject + +/// Whether Batch can resolve the user's region/location from the ip address. +/// This is for Batch internal use only since you will get a new instance each time you call +/// ``Batch/BatchSDK/updateAutomaticDataCollection:`` . +- (NSNumber *_Nullable)geoIPEnabled; + +/// Set whether Batch can resolve the user's region/location from the ip address. +/// +/// - Parameter geoIPEnabled: Whether Batch can resolve the GeoIP on server side. Default: false +- (void)setGeoIPEnabled:(BOOL)geoIPEnabled; + +/// Whether Batch can send the device model information. +/// This is for Batch internal use only since you will get a new instance each time you call +/// ``Batch/BatchSDK/updateAutomaticDataCollection:`` . +- (NSNumber *_Nullable)deviceModelEnabled; + +/// Set whether Batch can send the device model information. +/// +/// - Parameter deviceModelEnabled: Whether Batch can send the device model information. Default: false +- (void)setDeviceModelEnabled:(BOOL)deviceModelEnabled; + +@end + +/// Typed block that will be called with an instance of the automatic data collection configuration as a parameter. +typedef void (^BatchDataCollectionConfigEditor)(BatchDataCollectionConfig *); + +NS_ASSUME_NONNULL_END diff --git a/Sources/Batch/BatchDataCollectionConfig.m b/Sources/Batch/BatchDataCollectionConfig.m new file mode 100644 index 0000000..a737f53 --- /dev/null +++ b/Sources/Batch/BatchDataCollectionConfig.m @@ -0,0 +1,39 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +#import "BatchDataCollectionConfig.h" + +@interface BatchDataCollectionConfig () { + NSNumber *_geoIPEnabled; + NSNumber *_deviceModelEnabled; +} +@end + +@implementation BatchDataCollectionConfig + +- (instancetype)init { + self = [super init]; + _geoIPEnabled = nil; + _deviceModelEnabled = nil; + return self; +} + +- (NSNumber *_Nullable)geoIPEnabled { + return _geoIPEnabled; +} + +- (void)setGeoIPEnabled:(BOOL)geoIPEnabled { + _geoIPEnabled = [NSNumber numberWithBool:geoIPEnabled]; +} + +- (NSNumber *_Nullable)deviceModelEnabled { + return _deviceModelEnabled; +} + +- (void)setDeviceModelEnabled:(BOOL)deviceModelEnabled { + _deviceModelEnabled = [NSNumber numberWithBool:deviceModelEnabled]; +} +@end diff --git a/Sources/Batch/BatchEventData.h b/Sources/Batch/BatchEventAttributes.h similarity index 53% rename from Sources/Batch/BatchEventData.h rename to Sources/Batch/BatchEventAttributes.h index a34d8aa..07b0324 100644 --- a/Sources/Batch/BatchEventData.h +++ b/Sources/Batch/BatchEventAttributes.h @@ -1,5 +1,5 @@ // -// BatchEventData.h +// BatchEventAttributes.h // Batch // // https://batch.com @@ -10,17 +10,51 @@ NS_ASSUME_NONNULL_BEGIN -/// Object holding data to be associated to an event +/// Object holding attributes to be associated to an event /// /// - Note: Keys should be made of letters, numbers or underscores ([a-z0-9_]) and can't be longer than 30 characters. -@interface BatchEventData : NSObject +@interface BatchEventAttributes : NSObject -/// Add a tag. +/// Initialize an event data object. +- (instancetype)init; + +/// Initialize an event using block/closure. Convinence initializer for Swift usage. +- (instancetype)initWithBuilder:(void (^__nonnull)(BatchEventAttributes *))builder; + +/// Validate the event data. +/// - Parameter error: NSError to write to. If the validation succeeds, your variable will be set to nil. Otherwise, get +/// the error description for more info. Detailed error information is not available via error domain or code. +/// - Returns True if the event data validates successfully, false if not. If the data does not validate, Batch will +/// refuse to track an event with it. +- (BOOL)validateWithError:(NSError **)error; + +/// Add an array of string attribute for the specified key. +/// +/// - Parameters: +/// - value: Array of string values to add. Strings must not be longer than 200 characters. Cannot have more than 25 +/// items. +/// - key: Attribute key. Should be made of letters, numbers or underscores ([a-z0-9_]) and can't be longer than 30 +/// characters. +- (void)putStringArray:(nonnull NSArray *)value forKey:(NSString *)key NS_SWIFT_NAME(put(_:forKey:)); + +/// Add an array of objects attribute for the specified key. /// /// - Parameters: -/// - tag: Tag to add. Can't be longer than 64 characters, and can't be empty or null. For better results, you should -/// trim/lowercase your strings, and use slugs when possible. -- (void)addTag:(NSString *)tag NS_SWIFT_NAME(add(tag:)); +/// - value: Array of object values to add. Must be represented by BatchEventAttributes instances that are copied when +/// put. Warning: sub objects have more limitations than root BatchEventAttributes. Cannot have more than 25 items. +/// - key: Attribute key. Should be made of letters, numbers or underscores ([a-z0-9_]) and can't be longer than 30 +/// characters. +- (void)putObjectArray:(nonnull NSArray *)value + forKey:(NSString *)key NS_SWIFT_NAME(put(_:forKey:)); + +/// Add an object attribute for the specified key. +/// +/// - Parameters: +/// - value: Object value to add. Must be represented by a BatchEventAttributes instance. Warning: sub objects have +/// more limitations than root BatchEventAttributes. +/// - key: Attribute key. Should be made of letters, numbers or underscores ([a-z0-9_]) and can't be longer than 30 +/// characters. +- (void)putObject:(nonnull BatchEventAttributes *)value forKey:(NSString *)key NS_SWIFT_NAME(put(_:forKey:)); /// Add a boolean attribute for the specified key. /// diff --git a/Sources/Batch/BatchEventAttributes.m b/Sources/Batch/BatchEventAttributes.m new file mode 100644 index 0000000..8543246 --- /dev/null +++ b/Sources/Batch/BatchEventAttributes.m @@ -0,0 +1,231 @@ +#import +#import +#import +#import +#import + +#import + +#define PUBLIC_DOMAIN @"BatchProfile - Event Data" +#define DEBUG_DOMAIN @"BatchEventAttributes" + +@implementation BatchEventAttributes { + // Other ivars are in BatchEventDataPrivate +} + +- (instancetype)init { + self = [super init]; + if (self) { + _attributes = [NSMutableDictionary new]; + } + return self; +} + +- (instancetype)initWithBuilder:(void (^)(BatchEventAttributes *_Nonnull))builder { + self = [self init]; + if (builder) { + builder(self); + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + BatchEventAttributes *copy = [[[self class] allocWithZone:zone] init]; + if (copy) { + // We do not need a deep copy of the array as everything we store in it is either + // immutable or has already been copied + copy->_attributes = [_attributes mutableCopy]; + copy->_label = [_label copy]; + copy->_tags = [_tags copy]; + } + + return copy; +} + +- (BOOL)validateWithError:(NSError **)error { + NSArray *errors = + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] validateEventAttributes:self]; + + if ([errors count] > 0) { + NSString *errorDescription = [NSString + stringWithFormat:@"Failed to validate event attributes:\n\n%@", [errors componentsJoinedByString:@"\n"]]; + + if (error) { + *error = [NSError errorWithDomain:PROFILE_ERROR_DOMAIN + code:BatchProfileErrorInvalidEventAttributes + userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; + } + + return false; + } else { + if (error) { + *error = nil; + } + return true; + } +} + +- (void)putStringArray:(nonnull NSArray *)value forKey:(NSString *)key { + if (![value isKindOfClass:NSArray.class]) { + return; + } + + if (![self _areArrayElements:value ofType:NSString.class]) { + [BALogger publicForDomain:PUBLIC_DOMAIN + message:@"Could not add string array: invalid array element types (must be NSString)"]; + return; + } + + if ([@"$tags" isEqualToString:key]) { + _tags = [value copy]; + return; + } + + // This makes a deep copy as NSString conforms to NSCopying + NSArray *valueCopy = [[NSArray alloc] initWithArray:value copyItems:true]; + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:valueCopy type:BAEventAttributeTypeStringArray] + forKey:key]; +} + +- (void)putObjectArray:(nonnull NSArray *)value forKey:(NSString *)key { + if (![value isKindOfClass:NSArray.class]) { + return; + } + + if (![self _areArrayElements:value ofType:BatchEventAttributes.class]) { + [BALogger + publicForDomain:PUBLIC_DOMAIN + message:@"Could not add object array: invalid array element types (must be BatchEventAttributes)"]; + return; + } + + // This makes a deep copy as BatchEventAttributes conforms to NSCopying + NSArray *valueCopy = [[NSArray alloc] initWithArray:value copyItems:true]; + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:valueCopy type:BAEventAttributeTypeObjectArray] + forKey:key]; +} + +- (void)putObject:(nonnull BatchEventAttributes *)value forKey:(NSString *)key { + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[value copy] type:BAEventAttributeTypeObject] + forKey:key]; +} + +- (void)putBool:(BOOL)value forKey:(NSString *)key { + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithBool:value] + type:BAEventAttributeTypeBool] + forKey:key]; +} + +- (void)putInteger:(NSInteger)value forKey:(NSString *)key { + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithInteger:value] + type:BAEventAttributeTypeInteger] + forKey:key]; +} + +- (void)putFloat:(float)value forKey:(NSString *)key { + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithFloat:value] + type:BAEventAttributeTypeDouble] + forKey:key]; +} + +- (void)putDouble:(double)value forKey:(NSString *)key { + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithDouble:value] + type:BAEventAttributeTypeDouble] + forKey:key]; +} + +- (void)putString:(NSString *)value forKey:(NSString *)key { + if (![value isKindOfClass:NSString.class]) { + return; + } + + if ([@"$label" isEqualToString:key]) { + _label = [value copy]; + return; + } + + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[value copy] type:BAEventAttributeTypeString] + forKey:key]; +} + +- (void)putDate:(NSDate *)value forKey:(NSString *)key { + if (![value isKindOfClass:[NSDate class]]) { + [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot add a null or non NSDate date attribute. Ignoring."]; + return; + } + + NSNumber *timestamp = @(floor([value timeIntervalSince1970] * 1000)); + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:timestamp type:BAEventAttributeTypeDate] + forKey:key]; +} + +- (void)putURL:(NSURL *)value forKey:(NSString *)key { + if (![value isKindOfClass:[NSURL class]]) { + [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot add a null or non NSURL url attribute. Ignoring."]; + return; + } + + [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:value type:BAEventAttributeTypeURL] forKey:key]; +} + +- (void)_putTypedAttribute:(BATTypedEventAttribute *)attribute forKey:(NSString *)key { + _attributes[key.lowercaseString] = attribute; +} + +- (BOOL)_areArrayElements:(nonnull NSArray *)array ofType:(Class)clazz { + for (id element in array) { + if (![element isKindOfClass:clazz]) { + return false; + } + } + return true; +} + +- (nonnull NSDictionary *)_attributes { + return _attributes; +} + +- (nullable NSString *)_label { + return _label; +} + +- (nullable NSArray *)_tags { + return _tags; +} + +@end + +@implementation BATTypedEventAttribute : NSObject + ++ (nonnull instancetype)attributeWithValue:(NSObject *)value type:(BAEventAttributeType)type { + BATTypedEventAttribute *attr = [BATTypedEventAttribute new]; + attr.value = value; + attr.type = type; + return attr; +} + +- (NSString *)typeSuffix { + switch (self.type) { + case BAEventAttributeTypeBool: + return @"b"; + case BAEventAttributeTypeInteger: + return @"i"; + case BAEventAttributeTypeDouble: + return @"f"; + case BAEventAttributeTypeString: + return @"s"; + case BAEventAttributeTypeDate: + return @"t"; + case BAEventAttributeTypeURL: + return @"u"; + case BAEventAttributeTypeStringArray: + case BAEventAttributeTypeObjectArray: + return @"a"; + case BAEventAttributeTypeObject: + return @"o"; + default: + return @"x"; + } +} + +@end diff --git a/Sources/Batch/BatchEventDataPrivate.h b/Sources/Batch/BatchEventAttributesPrivate.h similarity index 62% rename from Sources/Batch/BatchEventDataPrivate.h rename to Sources/Batch/BatchEventAttributesPrivate.h index 7720378..24a381e 100644 --- a/Sources/Batch/BatchEventDataPrivate.h +++ b/Sources/Batch/BatchEventAttributesPrivate.h @@ -1,12 +1,12 @@ // -// BatchEventDataPrivate.h +// BatchEventAttributesPrivate.h // Batch // // https://batch.com // Copyright (c) 2016 Batch SDK. All rights reserved. // -#import +#import #define BA_EVENT_DATA_TAGS_KEY @"tags" #define BA_EVENT_DATA_ATTRIBUTES_KEY @"attributes" @@ -19,33 +19,38 @@ NS_ASSUME_NONNULL_BEGIN -@interface BatchEventData () { +@interface BatchEventAttributes () { @public NSMutableDictionary *_attributes; - NSMutableSet *_tags; - BOOL _convertedFromLegacy; + NSString *_label; + NSArray *_tags; } -- (void)_copyLegacyData:(NSDictionary *)data; +@property (readonly, nonnull) NSDictionary *_attributes; -- (NSDictionary *)_internalDictionaryRepresentation; +@property (readonly, nullable) NSString *_label; + +@property (readonly, nullable) NSArray *_tags; @end -typedef NS_ENUM(NSUInteger, BAEventAttributeType) { +typedef NS_CLOSED_ENUM(NSUInteger, BAEventAttributeType) { BAEventAttributeTypeString, BAEventAttributeTypeInteger, BAEventAttributeTypeDouble, BAEventAttributeTypeBool, BAEventAttributeTypeDate, BAEventAttributeTypeURL, + BAEventAttributeTypeStringArray, + BAEventAttributeTypeObjectArray, + BAEventAttributeTypeObject, }; @interface BATTypedEventAttribute : NSObject + (nonnull instancetype)attributeWithValue:(NSObject *)value type:(BAEventAttributeType)type; -- (NSString *)typeSuffix; +@property (readonly, nonnull) NSString *typeSuffix; @property (nonatomic, nonnull) NSObject *value; diff --git a/Sources/Batch/BatchEventData.m b/Sources/Batch/BatchEventData.m deleted file mode 100644 index 38517d9..0000000 --- a/Sources/Batch/BatchEventData.m +++ /dev/null @@ -1,334 +0,0 @@ -#import -#import - -#import - -#define MAXIMUM_VALUES 15 -#define MAXIMUM_TAGS 10 -#define MAXIMUM_STRING_LENGTH 64 -#define MAXIMUM_URL_LENGTH 2048 -#define ATTRIBUTE_KEY_RULE @"^[a-zA-Z0-9_]{1,30}$" - -#define PUBLIC_DOMAIN @"BatchUser - Event Data" -#define DEBUG_DOMAIN @"BatchEventData" - -@implementation BatchEventData { - // Other ivars are in BatchEventDataPrivate - NSRegularExpression *_attributeKeyValidationRegexp; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _convertedFromLegacy = false; - _attributes = [NSMutableDictionary new]; - _tags = [NSMutableSet new]; - - static NSRegularExpression *regex; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSError *error = nil; - regex = [NSRegularExpression regularExpressionWithPattern:ATTRIBUTE_KEY_RULE options:0 error:&error]; - if (error) { - // Something went really wrong, so we'll just throw internal errors - [BALogger errorForDomain:DEBUG_DOMAIN message:@"Error while creating event attribute key regexp."]; - regex = nil; - } - }); - - _attributeKeyValidationRegexp = regex; - } - return self; -} - -- (id)copyWithZone:(NSZone *)zone { - BatchEventData *copy = [[[self class] allocWithZone:zone] init]; - if (copy) { - copy->_convertedFromLegacy = _convertedFromLegacy; - copy->_attributes = [_attributes mutableCopy]; - copy->_tags = [_tags mutableCopy]; - } - - return copy; -} - -- (void)addTag:(NSString *)tag { - if ([self _enforceTagsCount:tag] && [self _enforceStringValue:tag]) { - [_tags addObject:[tag lowercaseString]]; - } -} - -- (void)putBool:(BOOL)value forKey:(NSString *)key { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithBool:value] - type:BAEventAttributeTypeBool] - forKey:key]; -} - -- (void)putInteger:(NSInteger)value forKey:(NSString *)key { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithInteger:value] - type:BAEventAttributeTypeInteger] - forKey:key]; -} - -- (void)putFloat:(float)value forKey:(NSString *)key { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithFloat:value] - type:BAEventAttributeTypeDouble] - forKey:key]; -} - -- (void)putDouble:(double)value forKey:(NSString *)key { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:[NSNumber numberWithDouble:value] - type:BAEventAttributeTypeDouble] - forKey:key]; -} - -- (void)putString:(NSString *)value forKey:(NSString *)key { - if ([self _enforceStringValue:value]) { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:value type:BAEventAttributeTypeString] - forKey:key]; - } -} - -- (void)putDate:(NSDate *)value forKey:(NSString *)key { - if ([self _enforceDateValue:value]) { - NSNumber *timestamp = @(floor([value timeIntervalSince1970] * 1000)); - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:timestamp type:BAEventAttributeTypeDate] - forKey:key]; - } -} - -- (void)putURL:(NSURL *)value forKey:(NSString *)key { - if ([self _enforceURLValue:value]) { - [self _putTypedAttribute:[BATTypedEventAttribute attributeWithValue:value.absoluteString - type:BAEventAttributeTypeURL] - forKey:key]; - } -} - -- (void)_putTypedAttribute:(BATTypedEventAttribute *)attribute forKey:(NSString *)key { - key = key.lowercaseString; - // If the key already exists, skip the checks - if ([_attributes objectForKey:key] != nil || ([self _enforceAttributesCount:key] && [self _enforceKey:key])) { - _attributes[key] = attribute; - } -} - -- (BOOL)_enforceStringValue:(NSString *)value { - if (value.length == 0) { - [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot add a null or empty string attribute/tag. Ignoring."]; - return false; - } - - if (value.length > MAXIMUM_STRING_LENGTH) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"String attributes and tags can't be longer than %d characters. Ignoring.", - MAXIMUM_STRING_LENGTH]; - return false; - } - - return true; -} - -- (BOOL)_enforceDateValue:(NSDate *)value { - if (![value isKindOfClass:[NSDate class]]) { - [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot add a null or non NSDate date attribute. Ignoring."]; - return false; - } - - return true; -} - -- (BOOL)_enforceURLValue:(NSURL *)value { - if (![value isKindOfClass:[NSURL class]]) { - [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot add a null or non NSURL url attribute. Ignoring."]; - return false; - } - - if ([(value.absoluteString) length] > MAXIMUM_URL_LENGTH) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"URL attributes can't be longer than %d characters. Ignoring.", MAXIMUM_URL_LENGTH]; - - return false; - } - - if (value.scheme == nil || value.host == nil) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"URL attributes must follow the format " - @"'scheme://[authority][path][?query][#fragment]'. Ignoring."]; - return false; - } - - return true; -} - -- (BOOL)_enforceKey:(NSString *)key { - if (_attributeKeyValidationRegexp == nil) { - [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Could not put attribute. Internal error."]; - return false; - } - - if ([key isKindOfClass:[NSString class]]) { - NSRange matchingRange = [_attributeKeyValidationRegexp rangeOfFirstMatchInString:key - options:0 - range:NSMakeRange(0, key.length)]; - if (matchingRange.location != NSNotFound) { - return true; - } else { - [BALogger - publicForDomain:PUBLIC_DOMAIN - message: - @"Invalid key. Please make sure that the key is made of letters, underscores and numbers " - @"only (a-zA-Z0-9_). It also can't be longer than 30 characters. Ignoring attribute '%@'.", - key]; - return false; - } - } - - return false; -} - -- (BOOL)_enforceAttributesCount:(NSString *)key { - if ([_attributes count] == MAXIMUM_VALUES) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Event data cannot hold more than 15 attributes. Ignoring attribute: '%@'", key]; - return false; - } - return true; -} - -- (BOOL)_enforceTagsCount:(NSString *)tag { - if ([_tags count] == MAXIMUM_TAGS) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Event data cannot hold more than 10 tags. Ignoring tag: '%@'", tag]; - return false; - } - return true; -} - -- (void)_copyLegacyData:(NSDictionary *)legacyData { - if (![legacyData isKindOfClass:[NSDictionary class]]) { - return; - } - - if (![NSJSONSerialization isValidJSONObject:legacyData]) { - [BALogger - debugForDomain:DEBUG_DOMAIN - message:@"Legacy event data is not a valid JSON Object according to NSJSONSerialization. Ignoring."]; - return; - } - - _convertedFromLegacy = true; - - NSArray *legacyDataKeys = legacyData.allKeys; - legacyDataKeys = [legacyDataKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; - - for (NSString *key in legacyDataKeys) { - NSObject *legacyValue = legacyData[key]; - - if ([_attributes count] >= MAXIMUM_VALUES) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Event data cannot hold more than 10 attributes. Stopping legacy conversion."]; - break; - } - - // Try to guess the legacy value type - if ([legacyValue isKindOfClass:[NSString class]]) { - [self putString:(NSString *)legacyValue forKey:key]; - } else if ([legacyValue isKindOfClass:[NSNumber class]]) { - NSNumber *numberAttr = (NSNumber *)legacyValue; - const char *ctype = [numberAttr objCType]; - - // Possible ctypes for NSNumber: “c”, “C”, “s”, “S”, “i”, “I”, “l”, “L”, “q”, “Q”, “f”, and “d”. - // Supported ones: "c", "s", "i", "l", "q", "f", "d" - - // Non decimal values are read as long long, which is the biggest on both 32 and 64-bit architectures - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Legacy data for key '%@' is a NSNumber: %s", key, ctype]; - if (strcmp(ctype, @encode(short)) == 0 || strcmp(ctype, @encode(int)) == 0 || - strcmp(ctype, @encode(long)) == 0 || strcmp(ctype, @encode(long long)) == 0) { - // Long long might be truncated on 32 bit platforms - [self putInteger:[numberAttr integerValue] forKey:key]; - } else if (strcmp(ctype, @encode(char)) == 0) { - // Usually chars are booleans, even shorts are stored as ints. - char val = [numberAttr charValue]; - if (val == 0 || val == 1) { - [self putBool:[numberAttr boolValue] forKey:key]; - } else { - [self putInteger:[numberAttr integerValue] forKey:key]; - } - } - // Decimal values - else if (strcmp(ctype, @encode(float)) == 0 || strcmp(ctype, @encode(double)) == 0) { - [self putDouble:[numberAttr doubleValue] forKey:key]; - } - // According to the documentation that's not supported, but give it a shot - else if (strcmp(ctype, @encode(BOOL)) == 0) { - [self putBool:[numberAttr boolValue] forKey:key]; - } else { - // Try to make it work in a NSInteger - NSInteger val = [numberAttr integerValue]; - if ([numberAttr isEqualToNumber:[NSNumber numberWithInteger:val]]) { - [self putInteger:[numberAttr integerValue] forKey:key]; - } - } - } else { - [BALogger debugForDomain:DEBUG_DOMAIN - message:@"Unsupported legacy attribute of class '%@' for key '%@'. Ignoring.", - NSStringFromClass([legacyValue class]), key]; - } - } -} - -- (NSDictionary *)_internalDictionaryRepresentation { - NSMutableDictionary *outAttributes = [NSMutableDictionary dictionaryWithCapacity:_attributes.count]; - - NSString *formattedKey; - BATTypedEventAttribute *typedAttr; - for (NSString *key in _attributes.keyEnumerator) { - typedAttr = _attributes[key]; - formattedKey = - [[[key lowercaseString] stringByAppendingString:@"."] stringByAppendingString:[typedAttr typeSuffix]]; - [outAttributes setObject:typedAttr.value forKey:formattedKey]; - } - - NSMutableDictionary *representation = [[NSMutableDictionary alloc] initWithCapacity:3]; - representation[BA_EVENT_DATA_TAGS_KEY] = [_tags allObjects]; - representation[BA_EVENT_DATA_ATTRIBUTES_KEY] = outAttributes; - - if (_convertedFromLegacy) { - representation[BA_EVENT_DATA_CONVERTED_KEY] = @(true); - } - - return representation; -} - -@end - -@implementation BATTypedEventAttribute : NSObject - -+ (nonnull instancetype)attributeWithValue:(NSObject *)value type:(BAEventAttributeType)type { - BATTypedEventAttribute *attr = [BATTypedEventAttribute new]; - attr.value = value; - attr.type = type; - return attr; -} - -- (NSString *)typeSuffix { - switch (self.type) { - case BAEventAttributeTypeBool: - return @"b"; - case BAEventAttributeTypeInteger: - return @"i"; - case BAEventAttributeTypeDouble: - return @"f"; - case BAEventAttributeTypeString: - return @"s"; - case BAEventAttributeTypeDate: - return @"t"; - case BAEventAttributeTypeURL: - return @"u"; - default: - return @"x"; - } -} - -@end diff --git a/Sources/Batch/BatchInbox.h b/Sources/Batch/BatchInbox.h index 04b4694..fdb2e80 100644 --- a/Sources/Batch/BatchInbox.h +++ b/Sources/Batch/BatchInbox.h @@ -40,20 +40,6 @@ /// notification is silent (shows nothing to the user). @property (nonatomic, readonly, nullable) BatchInboxNotificationContentMessage *message; -/// Notification title, if present. (Deprecated) -/// -/// - Warning: __Deprecated:__ The title should be accessed via the message property. -@property (nonatomic, readonly, nullable) NSString *title - __attribute__((deprecated("The title should be accessed via the message property"))); - -/// Notification alert body. (Deprecated) -/// -/// For compatibility, this will be the empty string when representing a silent notification, if their filtering has -/// been disabled on the fetcher. -/// - Warning: __Deprecated: __ The body should be accessed via the message property. -@property (nonatomic, readonly, nonnull) NSString *body - __attribute__((deprecated("The body should be accessed via the message property"))); - /// URL of the rich notification attachment (image/audio/video). @property (nonatomic, readonly, nullable) NSURL *attachmentURL; @@ -66,14 +52,6 @@ /// Flag indicating whether this notification is unread or not. @property (nonatomic, readonly) BOOL isUnread; -/// Flag indicating whether this notification is deleted or not. (Deprecated) -/// -/// This might change if you hold a pointer to a notification that you asked to be deleted. -/// - Warning: __Deprecated:__ You should refresh your copy of the data with -/// ``BatchInboxFetcher/allFetchedNotifications`` after using ``BatchInboxFetcher/markNotificationAsDeleted:``. -@property (nonatomic, readonly) BOOL isDeleted __attribute__((deprecated( - "You should refresh your copy of the data with allFetchedNotifications after using markNotificationAsDeleted."))); - /// Flag indicating whether this notification is silent or not. @property (nonatomic, readonly) BOOL isSilent; diff --git a/Sources/Batch/BatchInbox.m b/Sources/Batch/BatchInbox.m index 76ae4fb..bf93fea 100644 --- a/Sources/Batch/BatchInbox.m +++ b/Sources/Batch/BatchInbox.m @@ -145,7 +145,6 @@ - (nullable instancetype)initWithInternalIdentifier:(nonnull NSString *)identifi _payload = [rawPayload copy]; _date = date; _isUnread = isUnread; - _isDeleted = false; _failOnSilentNotification = failOnSilentNotification; if ([BANullHelper isStringEmpty:_identifier]) { @@ -191,12 +190,6 @@ - (BOOL)parseRawPayload { return false; } - _title = [_message title]; - - // "body" can't be nil as we would break the API. For compatibility, a nil body is represented as the empty string. - NSString *messageBody = [_message body]; - _body = messageBody != nil ? messageBody : @""; - return true; } @@ -305,10 +298,6 @@ - (void)_markAsRead { _isUnread = false; } -- (void)_markAsDeleted { - _isDeleted = true; -} - - (BOOL)hasLandingMessage { return [BatchMessaging messageFromPushPayload:_payload] != nil; } diff --git a/Sources/Batch/BatchInboxPrivate.h b/Sources/Batch/BatchInboxPrivate.h index 44e1868..36e39ad 100644 --- a/Sources/Batch/BatchInboxPrivate.h +++ b/Sources/Batch/BatchInboxPrivate.h @@ -29,6 +29,4 @@ - (void)_markAsRead; -- (void)_markAsDeleted; - @end diff --git a/Sources/Batch/BatchLogger.h b/Sources/Batch/BatchLogger.h index e61d5e5..d99f4d6 100644 --- a/Sources/Batch/BatchLogger.h +++ b/Sources/Batch/BatchLogger.h @@ -6,8 +6,8 @@ // Copyright (c) Batch SDK. All rights reserved. // -/// Protocol to implement if you want to use ``Batch/Batch/setLoggerDelegate:`` to get Batch logs in a custom object of -/// yours. +/// Protocol to implement if you want to use ``Batch/BatchSDK/setLoggerDelegate:`` to get Batch logs in a custom object +/// of yours. /// /// - Important: Be careful with your implementation: using this can impact stability and performance. You should only /// use it if you know what you are doing. diff --git a/Sources/Batch/BatchMessaging.h b/Sources/Batch/BatchMessaging.h index ac26ec9..5012f99 100644 --- a/Sources/Batch/BatchMessaging.h +++ b/Sources/Batch/BatchMessaging.h @@ -95,24 +95,21 @@ /// Sets Batch's messaging delegate. The delegate is used for optionaly informing your code about analytics event, or /// handling In-App messages manually. -/// -/// - Parameter delegate: Your messaging delegate, weakly retained. Set it to nil to remove it. -+ (void)setDelegate:(id _Nullable)delegate; +@property (class, nullable) id delegate; /// Toggles whether Batch should change the shared `AVAudioSession` configuration by itelf. /// /// It is used to avoid stopping the user's music when displaying a video inapp, /// but this may have undesirable effects on your app. -/// - Parameter canReconfigureAVAudioSession: Whether or not Batch can change the `AVAudioSession`. -+ (void)setCanReconfigureAVAudioSession:(BOOL)canReconfigureAVAudioSession; +@property (class) BOOL canReconfigureAVAudioSession; /// Toggles whether Batch should display the messaging views automatically when coming from a notification. +/// Default: true /// /// In-App messaging is not affected by this. If you want to manually display the In-App message, call /// ``BatchMessaging/setDelegate:`` with a delegate that implement ``BatchMessagingDelegate/batchInAppMessageReady:``. /// - Note: If automatic mode is enabled, manual integration methods will not work. -/// - Parameter isAutomaticModeOn: Whether to enable automatic mode or not. -+ (void)setAutomaticMode:(BOOL)isAutomaticModeOn NS_SWIFT_NAME(setAutomaticMode(on:)); +@property (class) BOOL automaticMode; /// Toogles whether BatchMessaging should enter its "do not disturb" (DnD) mode or exit it. /// @@ -220,7 +217,7 @@ @end /// BatchMessaging error code constants. -enum { +typedef NS_ENUM(NSInteger, BatchMessagingError) { /// The current iOS version is too old BatchMessagingErrorIncompatibleIOSVersion = -1001, @@ -243,6 +240,3 @@ enum { /// Batch is opted-out from BatchMessagingErrorOptedOut = -1007 }; - -/// @typedef BatchMessagingError -typedef NSInteger BatchMessagingError; diff --git a/Sources/Batch/BatchMessaging.m b/Sources/Batch/BatchMessaging.m index 8d87b7e..18ce575 100644 --- a/Sources/Batch/BatchMessaging.m +++ b/Sources/Batch/BatchMessaging.m @@ -481,14 +481,26 @@ + (void)setDelegate:(id _Nullable)delegate { [[BAMessagingCenter instance] setDelegate:delegate]; } ++ (id _Nullable)delegate { + return [[BAMessagingCenter instance] delegate]; +} + + (void)setCanReconfigureAVAudioSession:(BOOL)canReconfigureAVAudioSession { [[BAMessagingCenter instance] setCanReconfigureAVAudioSession:canReconfigureAVAudioSession]; } ++ (BOOL)canReconfigureAVAudioSession { + return [[BAMessagingCenter instance] canReconfigureAVAudioSession]; +} + + (void)setAutomaticMode:(BOOL)automatic { [[BAMessagingCenter instance] setAutomaticMode:automatic]; } ++ (BOOL)automaticMode { + return [[BAMessagingCenter instance] automaticMode]; +} + + (BOOL)doNotDisturb { return [BAMessagingCenter instance].doNotDisturb; } diff --git a/Sources/Batch/BatchProfile.h b/Sources/Batch/BatchProfile.h new file mode 100644 index 0000000..1bab1ad --- /dev/null +++ b/Sources/Batch/BatchProfile.h @@ -0,0 +1,148 @@ +// +// BatchProfile.h +// Batch +// +// https://batch.com +// Copyright (c) Batch SDK. All rights reserved. +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Error domain of `BatchProfile` related errors +FOUNDATION_EXPORT NSErrorDomain const _Nonnull BatchProfileErrorDomain; + +typedef NS_ERROR_ENUM(BatchProfileErrorDomain, BatchProfileError){ + /// Unknown internal error + BatchProfileErrorUnknown = -10, + + /// The event attributes failed to validate + /// See localizedDescription for more info + BatchProfileErrorInvalidEventAttributes = -21, + + /// The attribute key is invalid + /// See localizedDescription for more info + BatchProfileErrorEditorInvalidKey = -31, + + /// The value is invalid + /// See localizedDescription for more info + BatchProfileErrorEditorInvalidValue = -32, + + /// The editor has been consumed. Use a new instance to set attributes again. + /// See localizedDescription for more info + BatchProfileErrorEditorConsumed = -33, +}; + +/// Batch's Profile Module +@interface BatchProfile : NSObject + +/// Identifies this device with a profile using a Custom User ID +/// - Parameter customID: Custom user ID of the profile you want to identify against. If a profile already exists, this +/// device will be attached to it. Must not be longer than 1024 characters. ++ (void)identify:(nullable NSString *)customID; + +/// Track an event. +/// +/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the +/// server. +/// +/// - Parameters: +/// - eventName: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and can't be +/// longer than 30 characters. ++ (void)trackEventWithName:(nonnull NSString *)eventName NS_SWIFT_NAME(trackEvent(name:)); + +/// Track an event. +/// +/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the +/// server. +/// +/// - Parameters: +/// - eventName: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and can't be +/// longer than 30 characters. +/// - attributes: The event attributes (optional). ++ (void)trackEventWithName:(nonnull NSString *)eventName + attributes:(nullable BatchEventAttributes *)attributes NS_SWIFT_NAME(trackEvent(name:attributes:)); + +/// Track a geolocation update. +/// +/// You can call this method from any thread. Batch must be started at some point, or location updates won't be sent to +/// the server. You'll usually call this method in your `CLLocationManagerDelegate` implementation +/// - Parameter location: The user's location in the form of a `CLLocation` object, ideally the one provided by the +/// system to your delegate. ++ (void)trackLocation:(nonnull CLLocation *)location; + +/// Get the profile editor. +/// +/// There are two ways to use the editor: +/// +/// # As a closure +/// Call this method with a closure: you will get an instance of `BatchProfileEditor` as its parameters. +/// Once the closure returns, the profile data is saved. +/// Making the instance escape the closure is not supported: you might lose changes if you set data once the closure has +/// returned. +/// ```swift +/// BatchProfile.edit { editor in +/// try? editor.set(attribute: 26, forKey: "age") +/// // No need to call .save(), this is done automatically +/// } +/// ``` +/// +/// # Storing it in a local variable +/// Get an editor instance that you then call methods on. Handy for situations where you might need to wait on an async +/// function to get an attribute's value. Do not forget to call save once you're done changing the attributes, or they +/// will not be applied. +/// ```swift +/// // Get a new editor instance. +/// // You need to save this in a local variable until you call save +/// // Editor instances don't share changes, and calling save on an empty editor will do nothing +/// let editor = BatchProfile.editor() +/// // Set an attribute. try? allows a potential error to be silently ignored +/// // This example is a valid key/attribute pair, and will not throw an error. +/// try? editor.set(attribute: 26, forKey:"age") +/// editor.save() // Don't forget to save the changes +/// ``` +/// +/// Once save() has been called once (or implicitly when using the editor block), you will +/// not be able to set any attribute and must get a new instance. +/// +/// - Returns: A ``BatchProfileEditor`` instance ++ (nonnull BatchProfileEditor *)editor; + +/// Edit a profile. +/// +/// There are two ways to use `BatchProfileEditor`: +/// # As a closure +/// Call this method with a closure: you will get an instance of `BatchProfileEditor` as its parameters. +/// Once the closure returns, the profile data is saved. +/// Making the instance escape the closure is not supported: you might lose changes if you set data once the closure has +/// returned. +/// ```swift +/// BatchProfile.edit { editor in +/// try? editor.set(attribute: 26, forKey: "age") +/// // No need to call .save(), this is done automatically +/// } +/// ``` +/// +/// # Storing it in a local variable +/// Get an editor instance that you then call methods on. Handy for situations where you might need to wait on an async +/// function to get an attribute's value. Do not forget to call save once you're done changing the attributes, or they +/// will not be applied. +/// ```swift +/// // Get a new editor instance. +/// // You need to save this in a local variable until you call save +/// // Editor instances don't share changes, and calling save on an empty editor will do nothing +/// let editor = BatchProfile.editor() +/// // Set an attribute. try? allows a potential error to be silently ignored +/// // This example is a valid key/attribute pair, and will not throw an error. +/// try? editor.set(attribute: 26, forKey:"age") +/// editor.save() // Don't forget to save the changes +/// ``` ++ (void)editWithBlock:(void (^)(BatchProfileEditor *))editorClosure NS_SWIFT_NAME(editor(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Batch/BatchProfile.m b/Sources/Batch/BatchProfile.m new file mode 100644 index 0000000..4995117 --- /dev/null +++ b/Sources/Batch/BatchProfile.m @@ -0,0 +1,54 @@ +// Copyright (c) Batch SDK. All rights reserved. + +#import +#import +#import +#import +#import + +NSErrorDomain const BatchProfileErrorDomain = @"com.batch.ios.profile"; + +@interface BatchProfileEditor () + ++ (void)_editWithBlock:(nonnull void (^)(BatchProfileEditor *_Nonnull __strong))editorClosure; +- (instancetype)_initInternal; + +@end + +@implementation BatchProfile + ++ (void)identify:(nullable NSString *)customID { + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] identify:customID]; +} + ++ (void)trackEventWithName:(nonnull NSString *)eventName { + [self trackEventWithName:eventName attributes:nil]; +} + ++ (void)trackEventWithName:(nonnull NSString *)name attributes:(nullable BatchEventAttributes *)attributes { + if (![attributes isKindOfClass:[BatchEventAttributes class]]) { + return; + } + + NSError *error = nil; + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] trackPublicEventWithName:name + attributes:attributes + error:&error]; + if (error) { + [BALogger publicForDomain:@"Profile" message:@"trackEvent failed: %@", error.localizedDescription]; + } +} + ++ (void)trackLocation:(nonnull CLLocation *)location { + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] trackLocation:location]; +} + ++ (nonnull BatchProfileEditor *)editor { + return [[BatchProfileEditor alloc] _initInternal]; +} + ++ (void)editWithBlock:(nonnull void (^)(BatchProfileEditor *_Nonnull __strong))editorClosure { + [BatchProfileEditor _editWithBlock:editorClosure]; +} + +@end diff --git a/Sources/Batch/BatchProfileEditor.h b/Sources/Batch/BatchProfileEditor.h new file mode 100644 index 0000000..5f4b764 --- /dev/null +++ b/Sources/Batch/BatchProfileEditor.h @@ -0,0 +1,281 @@ +// +// BatchProfileEditor.h +// Batch +// +// https://batch.com +// Copyright (c) Batch SDK. All rights reserved. +// + +#import + +/// Enum defining the state of an email subscription +typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) { + BatchEmailSubscriptionStateSubscribed = 0, + BatchEmailSubscriptionStateUnsubscribed = 1, +}; + +/// Provides profile attribute edition methods. +/// +/// Once save() has been called once (or implicitly when using the editor block), you will +/// not be able to set any attribute and must get a new instance. +/// +/// # Setting an attribute +/// ```swift +/// // Get a new editor instance. +/// // You need to save this in a local variable until you call save +/// // Editor instances don't share changes, and calling save on an empty editor will do nothing +/// let editor = BatchProfile.editor() +/// // Set an attribute. try? allows a potential error to be silently ignored +/// // This example is a valid key/attribute pair, and will not throw an error. +/// try? editor.set(attribute: 26, forKey:"age") +/// do { +/// // Invalid attribute name, $ is a forbidden character +/// try editor.set(attribute: "patricia", forKey: "fir$t_name") +/// } catch { +/// // Handle the error here. +/// // Error is of type BatchProfileError if you want to specifically +/// // handle it. +/// } +/// editor.save() // Don't forget to save the changes +/// ``` +/// +/// # Removing an attribute +/// ```swift +/// // Get a new editor instance. +/// // You need to save this in a local variable until you call save +/// // Editor instances don't share changes, and calling save on an empty editor will do nothing +/// let editor = BatchProfile.editor() +/// try? editor.removeAttribute(forKey: "age") // Remove an attribute +/// editor.save() // Don't forget to save the changes +/// ``` +/// +/// # Managing array attributes +/// ```swift +/// let editor = BatchProfile.editor() +/// try? editor.set(attribute: ["apple_pay"], forKey: "payment_methods") +/// // Modify an existing array +/// try? editor.addToStringArray(value: ["carte_bleue"], forKey: "payment_methods") +/// try? editor.removeFromStringArray(value: ["carte_bleue"], forKey: "payment_methods") +/// editor.save(); // Don't forget to save the changes +/// ``` +/// +/// # Closure syntax +/// Another way to use the editor is by giving it a closure. Once the closure returns, the profile data is saved. +/// Making the instance escape the closure is not supported: you might lose changes if you set data once the closure has +/// returned. +/// ```swift +/// BatchProfile.editor { editor in +/// try editor.set(attribute: 26, forKey: "age") +/// // No need to call .save(), this is done automatically +/// } +/// ``` +@interface BatchProfileEditor : NSObject + +/// You cannot instantiate this class directly, please use `BatchProfile` +- (nonnull instancetype)init NS_UNAVAILABLE; + +/// Set a device language on a profile, overriding the detected one. +/// +/// - Parameter language: Lowercase, ISO 639 formatted string. nil to reset. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setLanguage:(nullable NSString *)language error:(NSError *_Nullable *_Nullable)error; + +/// Set a device region on a profile, overriding the detected one. +/// +/// - Parameter region: Region override: uppercase, ISO 3166 formatted string. nil to reset. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setRegion:(nullable NSString *)region error:(NSError *_Nullable *_Nullable)error; + +/// Set the user email. +/// +/// - Important: This method requires to already have a registered identifier for the user +/// or to call ``BatchProfile.identify()`` method before this one. +/// - Parameters: +/// - email: User email. +/// - error Pointer to an error describing. Note that the error is only about validation and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setEmailAddress:(nullable NSString *)email error:(NSError *_Nullable *_Nullable)error; + +/// Set the user email subscription state. +/// +/// - Parameters: +/// - state: Subscription state +- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state; + +/// Set a boolean profile attribute for a key. +/// +/// - Parameters: +/// - attribute: The attribute value. +/// - name: The attribute name. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setBooleanAttribute:(BOOL)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set a date profile attribute for a key. +/// +/// - Note: Since timezones are not supported, this will typically represent UTC dates. +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setDateAttribute:(nonnull NSDate *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set a string profile attribute for a key. +/// +/// Must not be longer than 64 characters. Can be empty. +/// For better results, you should make them upper/lowercase and trim the whitespaces. +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setStringAttribute:(nonnull NSString *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set a string array profile attribute for a key. +/// Can be empty. +/// +/// - Parameters: +/// - attribute: The attribute value. Cannot have more than 25 items. +/// Individual items cannot be longer than 64 characters. +/// For better results, you should make them upper/lowercase and trim the whitespaces. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setStringArrayAttribute:(nonnull NSArray *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Add a value to a string array. +/// Can be empty. +/// +/// If the attribute has been set to another type, it will be overwritten with a String array. +/// If the attribute does not exist, it will be created. +/// +/// - Parameters: +/// - attribute: The attribute value. Cannot have more than 25 operations pending on an array attribute. +/// Individual items cannot be longer than 64 characters. +/// For better results, you should make them upper/lowercase and trim the whitespaces. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)addItemToStringArrayAttribute:(nonnull NSString *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error + NS_SWIFT_NAME(addToStringArray(item:forKey:)); + +/// Remove a value from a string array. +/// Can be empty. +/// +/// If the attribute has been set to another type, it will be deleted. +/// +/// - Parameters: +/// - attribute: The attribute value. Cannot have more than 25 operations pending on an array attribute. +/// Individual items cannot be longer than 64 characters. +/// For better results, you should make them upper/lowercase and trim the whitespaces. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)removeItemFromStringArrayAttribute:(nonnull NSString *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error + NS_SWIFT_NAME(removeFromStringArray(item:forKey:)); + +/// Set an `NSInteger/Int` profile attribute for a key. +/// +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setIntegerAttribute:(NSInteger)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set an `long long/Int64` profile attribute for a key. +/// +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setLongLongAttribute:(long long)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set an float profile attribute for a key. +/// +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setFloatAttribute:(float)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set an double profile attribute for a key. +/// +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setDoubleAttribute:(double)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Set an url profile attribute for a key. +/// +/// Must not be longer than 2048 characters. Can't be empty or nil. +/// Must follow the format `scheme://[authority][path][?query][#fragment]`. +/// - Parameters: +/// - attribute: The attribute value. +/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and +/// can't be longer than 30 characters. +/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't +/// mean the value has been sent to the server yet. +/// - Returns: A boolean indicating whether the attribute passed validation or not. +- (BOOL)setURLAttribute:(nonnull NSURL *)attribute + forKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); + +/// Removes an attribute for the specified key. +/// +/// - Parameter key: The attribute key. Can't be nil. +- (BOOL)removeAttributeForKey:(nonnull NSString *)key + error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(removeAttribute(key:)); + +/// Save all of the pending changes made in that editor. +/// +/// - Important:This action cannot be undone. +- (void)save; + +@end diff --git a/Sources/Batch/BatchProfileEditor.m b/Sources/Batch/BatchProfileEditor.m new file mode 100644 index 0000000..eb32a48 --- /dev/null +++ b/Sources/Batch/BatchProfileEditor.m @@ -0,0 +1,217 @@ +// +// BatchUser.m +// Batch +// +// https://batch.com +// Copyright (c) 2015 Batch SDK. All rights reserved. +// + +#import +#import +#import +#import + +#define PUBLIC_DOMAIN @"BatchProfile - Editor" + +/// Those macros are required to properly catch a type cast error before Swift does it +/// as we can't catch a Swift bridging error + +#define ENSURE_KEY_STRING(attrKey) \ + if (![attrKey isKindOfClass:NSString.class]) { \ + *error = [self _logAndMakeSaveErrorWithCode:BatchProfileErrorEditorInvalidKey \ + reason:@"The attribute's key must be a nonnull NSString."]; \ + return false; \ + } + +#define ENSURE_ATTRIBUTE_VALUE_CLASS(attrValue, expectedClass) \ + if (attrValue == nil) { \ + *error = [self _logAndMakeSaveErrorWithCode:BatchProfileErrorEditorInvalidValue \ + reason:@"The attribute's value cannot be nil. Did you mean to use " \ + @"'removeAttributeForKey'?"]; \ + return false; \ + } \ + if (![attrValue isKindOfClass:expectedClass]) { \ + *error = [self _logAndMakeSaveErrorWithCode:BatchProfileErrorEditorInvalidValue \ + reason:@"The attribute's value isn't of the expected class (%@)", \ + NSStringFromClass(expectedClass)]; \ + return false; \ + } + +#define ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(attrValue, expectedClass) \ + if (attrValue != nil && ![attrValue isKindOfClass:expectedClass]) { \ + *error = [self _logAndMakeSaveErrorWithCode:BatchProfileErrorEditorInvalidValue \ + reason:@"The attribute's value isn't of the expected class (%@)", \ + NSStringFromClass(expectedClass)]; \ + return false; \ + } + +@implementation BatchProfileEditor { + BATProfileEditor *_backingImpl; +} + ++ (void)_editWithBlock:(nonnull void (^)(BatchProfileEditor *_Nonnull __strong))editorClosure { + [[[BatchProfileEditor alloc] _initInternal] _editWithBlock:editorClosure]; +} + +- (instancetype)_initInternal { + self = [super init]; + if (self) { + _backingImpl = [BAInjection injectClass:BATProfileEditor.class]; + // Enable install data compatibility + [_backingImpl enableInstallCompatibility]; + } + return self; +} + +- (BOOL)setLanguage:(nullable NSString *)language error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(language, NSString.class) + return [_backingImpl setLanguage:language error:error]; +} + +- (BOOL)setRegion:(nullable NSString *)region error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(region, NSString.class) + return [_backingImpl setRegion:region error:error]; +} + +- (BOOL)setEmailAddress:(nullable NSString *)email error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(email, NSString.class) + return [_backingImpl setEmail:email error:error]; +} + +- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state { + BATProfileEditorEmailSubscriptionState swiftState; + switch (state) { + case BatchEmailSubscriptionStateSubscribed: + swiftState = BATProfileEditorEmailSubscriptionStateSubscribed; + break; + case BatchEmailSubscriptionStateUnsubscribed: + swiftState = BATProfileEditorEmailSubscriptionStateUnsubscribed; + } + [_backingImpl setEmailMarketingSubscriptionState:swiftState]; +} + +- (BOOL)addItemToStringArrayAttribute:(NSString *)element forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl addWithValue:element toArray:key error:error]; +} + +- (BOOL)removeItemFromStringArrayAttribute:(NSString *)element forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl removeWithValue:element fromArray:key error:error]; +} + +- (BOOL)setBooleanAttribute:(BOOL)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl setCustomBoolAttribute:attribute forKey:key error:error]; +} + +- (BOOL)setDateAttribute:(nonnull NSDate *)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + ENSURE_ATTRIBUTE_VALUE_CLASS(attribute, NSDate.class) + return [_backingImpl setCustomDateAttribute:attribute forKey:key error:error]; +} + +- (BOOL)setStringAttribute:(NSString *)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + ENSURE_ATTRIBUTE_VALUE_CLASS(attribute, NSString.class) + return [_backingImpl setCustomStringAttribute:attribute forKey:key error:error]; +} + +- (BOOL)setStringArrayAttribute:(NSArray *)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + ENSURE_ATTRIBUTE_VALUE_CLASS(attribute, NSArray.class) + + if (![self _areArrayElements:attribute ofType:NSString.class]) { + *error = [self + _logAndMakeSaveErrorWithCode:BatchProfileErrorEditorInvalidValue + reason:@"String array attributes must only contain instances of String/NSString"]; + return false; + } + + return [_backingImpl setCustomStringArrayAttribute:attribute forKey:key error:error]; +} + +- (BOOL)setIntegerAttribute:(NSInteger)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl setCustomInt64Attribute:(int64_t)attribute forKey:key error:error]; +} + +- (BOOL)setLongLongAttribute:(long long)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl setCustomInt64Attribute:attribute forKey:key error:error]; +} + +- (BOOL)setFloatAttribute:(float)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl setCustomDoubleAttribute:(double)attribute forKey:key error:error]; +} + +- (BOOL)setDoubleAttribute:(double)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl setCustomDoubleAttribute:attribute forKey:key error:error]; +} + +- (BOOL)setURLAttribute:(nonnull NSURL *)attribute forKey:(NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + ENSURE_ATTRIBUTE_VALUE_CLASS(attribute, NSURL.class) + return [_backingImpl setCustomURLAttribute:attribute forKey:key error:error]; +} + +- (BOOL)removeAttributeForKey:(nonnull NSString *)key error:(NSError **)error { + INIT_AND_BLANK_ERROR_IF_NEEDED(error) + ENSURE_KEY_STRING(key) + return [_backingImpl deleteCustomAttributeForKey:key error:error]; +} + +- (void)save { + @synchronized(self) { + if (_backingImpl.consumed) { + [BALogger publicForDomain:PUBLIC_DOMAIN message:@"Cannot save editor: save has already been called once"]; + return; + } + [_backingImpl consume]; + // Copy the editor to make sure that it's not modified while we work with it + BATProfileEditor *editorCopy = [_backingImpl copy]; + + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] applyEditor:editorCopy]; + } +} + +- (void)_editWithBlock:(nonnull void (^)(BatchProfileEditor *_Nonnull __strong))editorClosure { + editorClosure(self); + [self save]; +} + +- (NSError *)_logAndMakeSaveErrorWithCode:(BatchProfileError)code reason:(NSString *)reasonFormatString, ... { + va_list arglist; + va_start(arglist, reasonFormatString); + NSString *reason = [[NSString alloc] initWithFormat:reasonFormatString arguments:arglist]; + va_end(arglist); + [BALogger publicForDomain:PUBLIC_DOMAIN message:@"%@", reason]; + return [NSError errorWithDomain:BatchProfileErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : reason}]; +} + +- (BOOL)_areArrayElements:(nonnull NSArray *)array ofType:(Class)clazz { + for (id element in array) { + if (![element isKindOfClass:clazz]) { + return false; + } + } + return true; +} + +@end diff --git a/Sources/Batch/BatchPush.h b/Sources/Batch/BatchPush.h index 75d739d..8aa6b91 100644 --- a/Sources/Batch/BatchPush.h +++ b/Sources/Batch/BatchPush.h @@ -34,10 +34,6 @@ FOUNDATION_EXPORT NSString *_Nonnull const BatchPushOpenedNotification; /// The value is a NSDictionary. FOUNDATION_EXPORT NSString *_Nonnull const BatchPushOpenedNotificationPayloadKey; -/// Key that Batch will read the placeholder from when converting a `UIUserNotificationAction` into a -/// `UNNotificationAction` -FOUNDATION_EXPORT NSString *_Nonnull const BatchUserActionInputTextFieldPlaceholderKey; - /// Notification sent by Batch Push when the alert view requesting users to allow push notifications was dismissed. /// /// Notification's userInfo will contain a ``BatchPushUserDidAcceptKey`` regarding the choice made by the user, whose @@ -80,15 +76,6 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// Do not call this method, as BatchPush only consists of static methods. - (nonnull instancetype)init NS_UNAVAILABLE; -/// Setup Batch Push system. (Deprecated) -/// -/// You can call this method from any thread. -/// - Warning: __Deprecated:__ This method is deprectaed. You don't need to do anything else besides removing this call, -/// Batch Push will still work as expected. -+ (void)setupPush - __attribute__((deprecated("setupPush is deprecated. You don't need to do anything else besides removing this call, " - "Batch Push will still work as expected."))); - /// Change the used remote notification types when registering. /// /// This does __NOT__ change the user preferences once already registered: to do so, you should point them to the native @@ -105,8 +92,52 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// You should call this at a strategic moment, like at the end of your welcome. /// /// Batch will automatically ask for a push token when the user replies. +/// +/// ```swift +/// func askPermission() { +/// BatchPush.requestNotificationAuthorization() +/// } +/// +/// func askPermissionAsync() async { +/// // Async methods always have to use the completionHandler variant +/// let _ = try? await BatchPush.requestNotificationAuthorization() +/// } +/// ``` +/// + (void)requestNotificationAuthorization; +/// Method to trigger the iOS popup that asks the user if they wants to allow notifications to be displayed, then +/// get a Push token. +/// +/// The default registration is made with Badge, Sound and Alert. +/// If you want another configuration: call ``BatchPush/setRemoteNotificationTypes:``. +/// You should call this at a strategic moment, like at the end of your welcome. +/// +/// Batch will automatically ask for a push token when the user replies and call your completion handler. +/// +/// ```swift +/// func askPermission() { +/// BatchPush.requestNotificationAuthorization { success, error in +/// // Do something +/// } +/// } +/// +/// func askPermissionAsync() async { +/// let _ = try? await BatchPush.requestNotificationAuthorization() +/// } +/// ``` +/// +/// - Parameters: +/// - completionHandler: The block to execute asynchronously with the results. This block may execute on a +/// background thread. The block has no return value and has the following parameters: +/// - completionHandler(granted): A Boolean value indicating whether the person grants authorization. The value of +/// this parameter is YES when the person grants authorization for one or more options. The value is NO when the +/// person denies authorization or authorization is undetermined. Use +/// `UNUserNotificationCenter.current().getNotificationSettings()` to check the authorization status. +/// - completionHandler(error): An object containing error information or nil if no error occurs. ++ (void)requestNotificationAuthorizationWithCompletionHandler: + (void (^_Nullable)(BOOL granted, NSError *__nullable error))completionHandler; + /// Method to ask iOS for a provisional notification authorization. /// /// Batch will then automatically ask for a push token. @@ -115,9 +146,54 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// They will directly be sent to the notification center, accessible when the user swipes up on the lockscreen, or down /// from the statusbar when unlocked. /// -/// This method does nothing on iOS 11 or lower. +/// ```swift +/// func askPermission() { +/// BatchPush.requestProvisionalNotificationAuthorization() +/// } +/// +/// func askPermissionAsync() async { +/// // Async methods always have to use the completionHandler variant +/// let _ = try? await BatchPush.requestProvisionalNotificationAuthorization() +/// } +/// ``` +/// + (void)requestProvisionalNotificationAuthorization; +/// Method to ask iOS for a provisional notification authorization. +/// +/// Batch will then automatically ask for a push token. +/// Provisional authorization will __NOT__ show a popup asking for user authorization, +/// but notifications will __NOT__ be displayed on the lock screen, or as a banner when the phone is unlocked. +/// They will directly be sent to the notification center, accessible when the user swipes up on the lockscreen, or down +/// from the statusbar when unlocked. +/// +/// Batch will automatically ask for a push token and call your completion handler. +/// +/// If a user has already refused to show notifications, this method will still call the completion handler with errors. +/// +/// ```swift +/// func askPermission() { +/// BatchPush.requestProvisionalNotificationAuthorization { success, error in +/// // Do something +/// } +/// } +/// +/// func askPermissionAsync() async { +/// let _ = try? await BatchPush.requestProvisionalNotificationAuthorization() +/// } +/// ``` +/// +/// - Parameters: +/// - completionHandler: The block to execute asynchronously with the results. This block may execute on a +/// background thread. The block has no return value and has the following parameters: +/// - completionHandler(granted): A Boolean value indicating whether the person grants authorization. The value of +/// this parameter is YES when the person grants authorization for one or more options. The value is NO when the +/// person denies authorization or authorization is undetermined. Use +/// `UNUserNotificationCenter.current().getNotificationSettings()` to check the authorization status. +/// - completionHandler(error): An object containing error information or nil if no error occurs. ++ (void)requestProvisionalNotificationAuthorizationWithCompletionHandler: + (void (^_Nullable)(BOOL granted, NSError *__nullable error))completionHandler; + /// Ask iOS to refresh the push token. If the app didn't prompt the user for consent yet, this will not be done. /// /// You should call this at the start of your app, to make sure Batch always gets a valid token after app updates. @@ -126,77 +202,30 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// Open the system settings on your applications' notification settings. + (void)openSystemNotificationSettings; -/// Method to trigger the iOS popup that asks the user if they wants to allow Push Notifications, then get a Push token. -/// (Deprecated) -/// -/// The default registration is made with Badge, Sound and Alert. -/// If you want another configuration: call`setRemoteNotificationTypes:`. -/// You should call this at a strategic moment, like at the end of your welcome. -/// Equivalent to calling ``BatchPush/requestNotificationAuthorization`` -/// - Warning: __Deprecated:__ This method is deprectaed. Use ``BatchPush/requestNotificationAuthorization`` and -/// ``BatchPush/refreshToken`` separately. More info in our documentation. -+ (void)registerForRemoteNotifications __attribute__(( - deprecated("Use requestNotificationAuthorization and refreshToken separately. More info in our documentation."))); - -/// Method to trigger the iOS popup that asks the user if they want to allow Push Notifications and register to APNS. -/// (Deprecated) -/// -/// Default registration is made with Badge, Sound and Alert. -/// If you want another configuration: call ``BatchPush/setRemoteNotificationTypes:``. -/// You should call this at a strategic moment, like at the end of your welcome. -/// -/// - Parameter categories: A set of `UIUserNotificationCategory` or `UNNotificationCategory` instances that define the -/// groups of actions a notification may include. If you try to register `UIUserNotificationCategory` instances on iOS -/// 10, Batch will automatically do a best effort conversion to `UNNotificationCategory`. If you don't want this -/// behaviour, please use the standard `UIApplication` methods. -/// - Warning: __Deprecated:__ Use ``BatchPush/setNotificationsCategories:`` and -/// ``BatchPush/registerForRemoteNotifications`` separately. -+ (void)registerForRemoteNotificationsWithCategories:(nullable NSSet *)categories - - __attribute__((deprecated("Use setNotificationCategories and registerForRemoteNotifications separately."))); - -/// Set the notification action categories to iOS. -/// -/// You should call this every time your app starts. -/// - Important: On versions prior to iOS 10, this call __MUST__ be followed by -/// ``BatchPush/registerForRemoteNotifications``, or else the categories will __NOT__ be updated. -/// - Parameter categories: set of `UIUserNotificationCategory` or `UNNotificationCategory` instances that define the -/// groups of actions a notification may include. If you try to register `UIUserNotificationCategory` instances on iOS -/// 10, Batch will automatically do a best effort conversion to `UNNotificationCategory`. If you don't want this -/// behaviour, please use the standard `UIApplication` methods. -+ (void)setNotificationsCategories:(nullable NSSet *)categories; - /// Clear the application's badge on the homescreen. -/// -/// You do not need to call this if you already called ``BatchPush/dismissNotifications``. + (void)clearBadge; -/// Clear the app's notifications in the notification center. Also clears your badge. -/// -/// Call this when you want to remove the notifications. Your badge is removed afterwards, so if you want one, you need -/// to set it up again. -/// - Important: Be careful, this method also clears your badge. +/// Clear the app's notifications in the notification center + (void)dismissNotifications; /// Set whether Batch Push should automatically try to handle deeplinks. /// -/// By default, this is set to __YES__. You need to call everytime your app is restarted, this option is not persisted. +/// By default, this is set to __true__. You need to call everytime your app is restarted, this option is not persisted. /// -/// If your goal is to implement a custom deeplink format, you should see ``Batch/Batch/deeplinkDelegate`` which allows -/// you to manually handle the deeplink string, but doesn't put the burden of parsing the notification payload on you. +/// If your goal is to implement a custom deeplink format, you should see ``Batch/BatchSDK/deeplinkDelegate`` which +/// allows you to manually handle the deeplink string, but doesn't put the burden of parsing the notification payload on +/// you. If deeplink handling is disabled, the deeplink delegate will not be called. /// /// - Note: Setting this to false will __DISABLE__ the deeplink delegate, leaving the handling of the link entirely up /// to you. -/// - Important: If Batch is set to handle your deeplinks, it will *automatically* call the fetch completion handler (if -/// applicable) with `UIBackgroundFetchResultNewData. /// - Parameter handleDeeplinks: Whether Batch should handle deeplinks automatically. -+ (void)enableAutomaticDeeplinkHandling:(BOOL)handleDeeplinks; +@property (class) BOOL enableAutomaticDeeplinkHandling; /// Get Batch Push's deeplink from a notification's userInfo. /// /// - Parameter userData The notification's payload. /// - Returns: Batch's Deeplink, or nil if not found. -+ (nullable NSString *)deeplinkFromUserInfo:(nonnull NSDictionary *)userData; ++ (nullable NSString *)deeplinkFromUserInfo:(nonnull NSDictionary *)userData NS_SWIFT_NAME(deeplink(from:)); /// Get the last known push token. /// @@ -204,13 +233,12 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// - Important: The returned token might be outdated and invalid if this method is called too early in your application /// lifecycle. /// - Returns: A push token, nil if unavailable. -+ (nullable NSString *)lastKnownPushToken; +@property (class, nullable, readonly) NSString *lastKnownPushToken; /// Disable the push's automatic integration. /// -/// If you call this, you are responsible of forwarding your application's delegate and -/// `UNUserNotificationCenterDelegate` calls to Batch. If you don't, some parts of the SDK and Dashboard will break. -/// Calling this method automatically calls `disableAutomaticNotificationCenterIntegration`. +/// If you call this, you are responsible of forwarding your application's delegate methods to Batch.If you don't, some +/// parts of the SDK and Dashboard will break. /// - Important: This must be called before you start Batch, or it will have no effect. + (void)disableAutomaticIntegration; @@ -231,48 +259,6 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { /// - Returns: Wheter it is a Batch'sPush. If it returns true, you should not handle the push. + (BOOL)isBatchPush:(nonnull NSDictionary *)userInfo; -/// Make Batch process a notification. (Deprecated) -/// -/// You should call this method in `application:didReceiveRemoteNotification:` or -/// `application:didReceiveRemoteNotification:fetchCompletionHandler:`. -/// - Important: If you didn't call ``BatchPush/disableAutomaticIntegration``, this method will have no effect. -/// If you called it but don't implement this method, Batch's push features will __NOT__ work. -/// - Parameter userInfo: The untouched `userInfo` NSDictionary argument given to you in the application delegate -/// method. -/// - Warning: __Deprecated:__ Implement `UNUserNotificationCenterDelegate` using -/// ``BatchUNUserNotificationCenterDelegate`` or your own implementation. -+ (void)handleNotification:(nonnull NSDictionary *)userInfo - __attribute__((deprecated("Implement UNUserNotificationCenterDelegate using BatchUNUserNotificationCenterDelegate " - "or your own implementation"))); - -/// Make Batch process a notification action. -/// -/// You should call this method in `application:handleActionWithIdentifier:forRemoteNotification:completionHandler:` -/// or `application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:` -/// - Important: If you didn't call "disableAutomaticIntegration", this method will have no effect. If you called it but -/// don't implement this method, Batch's push features will NOT work. -/// - Parameters: -/// - userInfo: The untouched `userInfo` NSDictionary argument given to you in the application delegate method. -/// - identifier: The action's identifier. Used for tracking purposes: it can match your raw action name, or be a more -/// user-friendly string. -+ (void)handleNotification:(nonnull NSDictionary *)userInfo actionIdentifier:(nullable NSString *)identifier; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -/// Make Batch process the user notification settings change. (Deprecated) -/// -/// You should call this method in `application:didRegisterUserNotificationSettings:`. -/// - Important: If you didn't call ``BatchPush/disableAutomaticIntegration``, this method will have no effect. If you -/// called it but don't implement this method, Batch's push features will __NOT__ work. -/// - Parameter notificationSettings: The untouched `notificationSettings UIUserNotificationSettings*` argument given to -/// you in the application delegate method. -/// - Warning: __Deprecated:__ Use ``BatchPush/requestNotificationAuthorization``. -+ (void)handleRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings - __attribute__((deprecated("Use BatchPush.requestNotificationAuthorization()"))); - -#pragma clang diagnostic pop - #pragma mark UserNotifications methods /// Make Batch process a foreground notification. @@ -301,6 +287,19 @@ typedef NS_ENUM(NSUInteger, BatchNotificationSource) { didReceiveNotificationResponse:(nonnull UNNotificationResponse *)response NS_SWIFT_NAME(handle(userNotificationCenter:didReceive:)); +/// Check a notification comes from Batch +/// +/// - Parameter notification: The UNNotification got from the operating system +/// method. +/// - Returns: Wheter it is a notification coming from Batch ++ (BOOL)isBatchNotification:(nonnull UNNotification *)notification; + +/// Get the Batch deeplink from a notification +/// +/// - Parameter notification The notification +/// - Returns: deeplink string, or nil if not found. Warning: the deeplink might not be a valid URL. ++ (nullable NSString *)deeplinkFromNotification:(nonnull UNNotification *)userData; + @end /// Implementation of `UNUserNotificationCenterDelegate` that @@ -325,7 +324,7 @@ __attribute__((objc_subclassing_restricted)) + (void)registerAsDelegate; /// Should iOS display notifications even if the app is in foreground? -/// Default: false +/// Default: true @property (assign) BOOL showForegroundNotifications; @end diff --git a/Sources/Batch/BatchPush.m b/Sources/Batch/BatchPush.m index 53a85e3..2e8ea65 100644 --- a/Sources/Batch/BatchPush.m +++ b/Sources/Batch/BatchPush.m @@ -11,7 +11,6 @@ #import #import #import -#import NSString *const BatchPushReceivedNotification = @"BatchPushReceivedNotification"; @@ -28,24 +27,6 @@ @implementation BatchPush -+ (BatchPushNotificationSettingStatus)notificationSettingStatus { - return [BANotificationAuthorization applicationSettings]; -} - -+ (void)setNotificationSettingStatus:(BatchPushNotificationSettingStatus)status { - switch (status) { - case BatchPushNotificationSettingStatusUndefined: - case BatchPushNotificationSettingStatusEnabled: - case BatchPushNotificationSettingStatusDisabled: - [[[[BACoreCenter instance] status] notificationAuthorization] setApplicationSettings:status - skipServerEvent:false]; - break; - default: - [BALogger errorForDomain:@"BatchPush" message:@"Invalid BatchPushNotificationSettingStatus value"]; - break; - } -} - + (void)setupPush { // This used to set Batch Push's state to YES, but now we forcibly enable the push } @@ -64,11 +45,21 @@ + (BOOL)supportsAppNotificationSettings { } + (void)requestNotificationAuthorization { - [[BAPushCenter instance] requestNotificationAuthorization]; + [[BAPushCenter instance] requestNotificationAuthorizationWithCompletionHandler:nil]; +} + ++ (void)requestNotificationAuthorizationWithCompletionHandler: + (void (^_Nullable)(BOOL granted, NSError *__nullable error))completionHandler { + [[BAPushCenter instance] requestNotificationAuthorizationWithCompletionHandler:completionHandler]; } + (void)requestProvisionalNotificationAuthorization { - [[BAPushCenter instance] requestProvisionalNotificationAuthorization]; + [[BAPushCenter instance] requestProvisionalNotificationAuthorizationWithCompletionHandler:nil]; +} + ++ (void)requestProvisionalNotificationAuthorizationWithCompletionHandler: + (void (^_Nullable)(BOOL granted, NSError *__nullable error))completionHandler { + [[BAPushCenter instance] requestProvisionalNotificationAuthorizationWithCompletionHandler:completionHandler]; } + (void)refreshToken { @@ -79,20 +70,6 @@ + (void)openSystemNotificationSettings { [[BAPushCenter instance] openSystemNotificationSettings]; } -// Call to trigger the iOS popup that asks the user if he wants to allow Push Notifications. -+ (void)registerForRemoteNotifications { - [[BAPushCenter instance] requestNotificationAuthorization]; -} - -+ (void)registerForRemoteNotificationsWithCategories:(NSSet *)categories { - [BAPushCenter setNotificationsCategories:categories]; - [[BAPushCenter instance] requestNotificationAuthorization]; -} - -+ (void)setNotificationsCategories:(NSSet *)categories { - [BAPushCenter setNotificationsCategories:categories]; -} - // Clear the application's badge on the homescreen. + (void)clearBadge { [BAPushCenter clearBadge]; @@ -104,8 +81,12 @@ + (void)dismissNotifications { } // Disable Batch's automatic deeplink handling -+ (void)enableAutomaticDeeplinkHandling:(BOOL)handleDeeplinks { - [BAPushCenter enableAutomaticDeeplinkHandling:handleDeeplinks]; ++ (void)setEnableAutomaticDeeplinkHandling:(BOOL)handleDeeplinks { + [[BAPushCenter instance] setHandleDeeplinks:handleDeeplinks]; +} + ++ (BOOL)enableAutomaticDeeplinkHandling { + return [[BAPushCenter instance] handleDeeplinks]; } + (NSString *)deeplinkFromUserInfo:(NSDictionary *)userInfo { @@ -134,21 +115,6 @@ + (BOOL)isBatchPush:(NSDictionary *)userInfo { return [BAPushCenter isBatchPush:userInfo]; } -+ (void)handleNotification:(NSDictionary *)userInfo { - [BAPushCenter handleNotification:userInfo]; -} - -+ (void)handleNotification:(NSDictionary *)userInfo actionIdentifier:(NSString *)identifier { - [BAPushCenter handleNotification:userInfo actionIdentifier:identifier]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -+ (void)handleRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - [BAPushCenter handleRegisterUserNotificationSettings:notificationSettings]; -} -#pragma clang diagnostic pop - + (void)handleUserNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification willShowSystemForegroundAlert:(BOOL)willShowSystemForegroundAlert { @@ -162,6 +128,14 @@ + (void)handleUserNotificationCenter:(UNUserNotificationCenter *)center [BAPushCenter handleUserNotificationCenter:center didReceiveNotificationResponse:response]; } ++ (BOOL)isBatchNotification:(nonnull UNNotification *)notification { + return [BAPushCenter isBatchPush:notification.request.content.userInfo]; +} + ++ (nullable NSString *)deeplinkFromNotification:(nonnull UNNotification *)notification { + return [BAPushCenter deeplinkFromUserInfo:notification.request.content.userInfo]; +} + @end @implementation BatchUNUserNotificationCenterDelegate @@ -183,7 +157,7 @@ + (void)registerAsDelegate { - (instancetype)init { self = [super init]; if (self) { - _showForegroundNotifications = false; + _showForegroundNotifications = true; } return self; } diff --git a/Sources/Batch/BatchPushPrivate.h b/Sources/Batch/BatchPushPrivate.h deleted file mode 100644 index a8aeb0b..0000000 --- a/Sources/Batch/BatchPushPrivate.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// BatchPushPrivate.h -// Batch -// -// https://batch.com -// Copyright (c) 2016 Batch SDK. All rights reserved. -// - -// Expose private constructors -// This header is private and should NEVER be distributed within the framework - -#import - -/// Remote notification setting status -/// -/// See `[BatchPush notificationSettingStatus]` for more information. -typedef NS_ENUM(NSUInteger, BatchPushNotificationSettingStatus) { - - /// User did not make a choice yet. Default value. - BatchPushNotificationSettingStatusUndefined = 0, - - /// User enabled notifications. - BatchPushNotificationSettingStatusEnabled = 1, - - /// User disabled notifications. - BatchPushNotificationSettingStatusDisabled = 2, -}; - -@interface BatchPush () - -/// This property defines the status of the notification setting (or opt-in/opt-out) in your application. -/// It will be sent to the server, to let Batch know whether a user enabled or disabled notifications globally in your -/// app's settings. You can use it to implement your own "Notifications" setting toggle. - -/// This property has a default value of BatchPushNotificationSettingStatusUndefined, and will automatically change to -/// BatchPushNotificationSettingStatusEnabled if you prompt the user to allow notifications using Batch's methods. -/// If you have previously overriden the value to BatchPushNotificationSettingStatusDisabled, the property will not -/// change. - -/// - Note: This does NOT reflect the system settings. If the user opted out from iOS' settings, you will still get the -/// original value you set previously here. You will need to handle this case in your settings, and redirect the user to -/// the system settings. -@property (class) BatchPushNotificationSettingStatus notificationSettingStatus; - -@end diff --git a/Sources/Batch/BatchUser.h b/Sources/Batch/BatchUser.h index c85c2c7..1b304eb 100644 --- a/Sources/Batch/BatchUser.h +++ b/Sources/Batch/BatchUser.h @@ -6,8 +6,8 @@ // Copyright (c) Batch SDK. All rights reserved. // -#import -#import +#import +#import #import @class BatchUserDataEditor, BatchUserAttribute; @@ -22,56 +22,27 @@ FOUNDATION_EXPORT NSString *_Nonnull const BatchEventTrackerFinishedNotification /// sent events to the server or not. FOUNDATION_EXPORT NSString *_Nonnull const BatchEventTrackerFinishedWithSuccessKey; -/// Error domain of error when ``BatchUserDataEditor`` fail. -FOUNDATION_EXPORT NSErrorDomain const _Nonnull BatchUserDataEditorErrorDomain; - -/// User data editor error codes. -typedef NS_ERROR_ENUM(BatchUserDataEditorErrorDomain, BatchUserDataEditorError){ - - /// Internal error. - BatchUserDataEditorErrorInternal = 0, - - /// The key is invalid. This also applies to tag collection names, as they're considered keys. - BatchUserDataEditorErrorInvalidKey = 1, - - /// The value is invalid: see the error description for more info. - BatchUserDataEditorErrorInvalidValue = 2, -}; - -/// Enum defining the state of an email subscription -typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) { - BatchEmailSubscriptionStateSubscribed = 0, - BatchEmailSubscriptionStateUnsubscribed = 1, -}; - /// Batch's User Module @interface BatchUser : NSObject /// Get the unique installation ID, generated by Batch. /// /// - Returns: Batch-generated installation ID. Might be nil if Batch isn't started. -+ (nullable NSString *)installationID; - -/// Get the user data editor. -/// -/// Do not forget to call save once you're done changing the attributes -/// You can call this method from any thread. -/// - Returns: A ``BatchUserDataEditor`` instance -+ (nonnull BatchUserDataEditor *)editor; +@property (class, nullable, readonly) NSString *installationID; /// Get the user language /// -/// - Returns: The custom language set with ``BatchUserDataEditor``, or nil if none was set. +/// - Returns: The custom language set with ``BatchProfile.editor``, or nil if none was set. + (nonnull NSString *)language; /// Get the user region /// -/// - Returns: The region set with ``BatchUserDataEditor``, or nil if none was set. +/// - Returns: The region set with ``BatchProfile.editor``, or nil if none was set. + (nonnull NSString *)region; /// Get the custom user identifier /// -/// - Returns: The custom identifier set with ``BatchUserDataEditor``, or nil if none was set. +/// - Returns: The custom identifier set with ``BatchProfile.editor``, or nil if none was set. + (nullable NSString *)identifier; /// Read the saved attributes. Reading is asynchronous so as not to interfere with saving operations. @@ -91,364 +62,8 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) { + (void)fetchTagCollections: (void (^_Nonnull)(NSDictionary *> *_Nullable collections))completionHandler; -/// Track an event. -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// - Parameter event: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) -/// and can't be longer than 30 characters. -+ (void)trackEvent:(nonnull NSString *)event; - -/// Track an event. -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// - Parameters: -/// - event: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and can't be longer -/// than 30 characters. -/// - label: The event label (optional). Must be a string. -+ (void)trackEvent:(nonnull NSString *)event withLabel:(nullable NSString *)label; - -/// Track an event. (Deprecated) -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// - Parameters: -/// - event: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and can't be longer -/// than 30 characters. -/// - label: The event label (optional). Must be a string. -/// - data: The event data (optional). Must a serializable JSON Foundation dictionary (meaning that it must pass -/// `+[NSJSONSerialization isValidJSONObject]`'s check). See `NSJSONSerialization` documentation for supported types, -/// with the only difference that the top level object __MUST BE__ a NSDictionary and not a NSArray. -/// - Warning: __Deprecated:__ See ``BatchUser/trackEvent:withLabel:associatedData:``. Data sent using this method -/// might be truncated if it can't be converted to a BatchEventData instance. -+ (void)trackEvent:(nonnull NSString *)event - withLabel:(nullable NSString *)label - data:(nullable NSDictionary *)data __attribute__((deprecated( - "Please migrate to [BatchUser trackEvent:withLabel:associatedData:]. Data sent using this " - "method might be truncated if it can't be converted to a BatchEventData instance."))); - -/// Track an event. -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// -/// - Parameters: -/// - event: The event name. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and can't be longer -/// than 30 characters. -/// - label: The event label (optional). Must be a string. -/// - data: The event data (optional). -+ (void)trackEvent:(nonnull NSString *)event - withLabel:(nullable NSString *)label - associatedData:(nullable BatchEventData *)data NS_SWIFT_NAME(trackEvent(_:withLabel:data:)); - -/// Track a transaction -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// - Parameter amount: Transaction's amount. -+ (void)trackTransactionWithAmount:(double)amount; - -/// Track a transaction. -/// -/// You can call this method from any thread. Batch must be started at some point, or events won't be sent to the -/// server. -/// - Parameters: -/// - amount: Transaction's amount. -/// - data: The transaction data (optional). Must a serializable JSON Foundation dictionary (meaning that it must pass -/// `+[NSJSONSerialization isValidJSONObject]`'s check). See `NSJSONSerialization` documentation for supported types, -/// with the only difference that the top level object __MUST BE__ a NSDictionary and not a NSArray. -+ (void)trackTransactionWithAmount:(double)amount data:(nullable NSDictionary *)data; - -/// Track a geolocation update. -/// -/// You can call this method from any thread. Batch must be started at some point, or location updates won't be sent to -/// the server. You'll usually call this method in your `CLLocationManagerDelegate` implementation -/// - Parameter location: The user's location in the form of a `CLLocation` object, ideally the one provided by the -/// system to your delegate. -+ (void)trackLocation:(nonnull CLLocation *)location; - -/// Print the currently known attributes and tags for a user to the logs (stderr/syslog). -+ (void)printDebugInformation; - -@end - -/// Provides user custom data methods -/// -/// # Setting an attribute -/// ```swift -/// // Get a new editor instance. -/// // You need to save this in a local variable until you call save -/// // Editor instances don't share changes, and calling save on an empty editor will do nothing -/// let editor = BatchUser.editor() -/// // Set an attribute. try? allows a potential error to be silently ignored -/// // This example is a valid key/attribute pair, and will not throw an error. -/// try? editor.set(attribute: 26, forKey:"age") -/// do { -/// // Invalid attribute name, $ is a forbidden character -/// try editor.set(attribute: "patricia", forKey: "fir$t_name") -/// } catch { -/// // Handle the error here. -/// // Error is of type BatchUserDataEditorError if you want to specifically -/// // handle it. -/// } -/// editor.save() // Don't forget to save the changes -/// ``` -/// -/// # Removing an attribute -/// ```swift -/// // Get a new editor instance. -/// // You need to save this in a local variable until you call save -/// // Editor instances don't share changes, and calling save on an empty editor will do nothing -/// let editor = BatchUser.editor() -/// editor.removeAttribute(forKey: "age") // Remove an attribute -/// editor.clearAttributes() // Removes all attributes -/// editor.save() // Don't forget to save the changes -/// ``` -/// -/// # Managing tag collections -/// ```swift -/// // Get a new editor instance. -/// // You need to save this in a local variable until you call save -/// // Editor instances don't share changes, and calling save on an empty editor will do nothing -/// let editor = BatchUser.editor() -/// editor.addTag("has_bought", inCollection: "actions") // Add a tag to the "actions" collection -/// editor.removeTag("has_bought", fromCollection: "actions") // Remove it -/// editor.clearTagCollection("actions") // Removes all tags from that collection -/// // editor.clearTags() // Removes all tag collections and tags -/// editor.save(); // Don't forget to save the changes -/// ``` -@interface BatchUserDataEditor : NSObject - -/// Override the detected user language. -/// -/// - Parameter language: Language override: lowercase, ISO 639 formatted string. nil to reset. -- (void)setLanguage:(nullable NSString *)language; - -/// Override the detected user region. -/// - Parameter region: Region override: uppercase, ISO 3166 formatted string. nil to reset. -- (void)setRegion:(nullable NSString *)region; - -/// Set the user identifier. -/// -/// - Important: Be careful: you should make sure the identifier uniquely identifies a user. When pushing an identifier, -/// all installations with that identifier will get the Push, which can cause some privacy issues if done wrong. -/// - Parameter identifier: User identifier. -- (void)setIdentifier:(nullable NSString *)identifier; - -/// Set the user attribution identifier. -/// -/// - Parameter attributionID: A valid UUID string or null to reset. -- (void)setAttributionIdentifier:(nullable NSString *)attributionID; - -/// Set the user email. -/// -/// - Important: This method requires to already have a registered identifier for the user -/// or to call ``BatchUserDataEditor/setIdentifier:`` method before this one. -/// - Parameters: -/// - email: User email. -/// - error Pointer to an error describing. Note that the error is only about validation and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setEmail:(nullable NSString *)email error:(NSError *_Nullable *_Nullable)error; - -/// Set the user email subscription state. -/// -/// - Parameters: -/// - state: Subscription state -- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state; - -/// Set a custom user attribute for a key. -/// -/// The attribute can be one of the following types, or their native Swift equivalent. -/// - NSString -/// - Must not be longer than 64 characters. Can be empty. -/// - For better results, you should make them upper/lowercase and trim the whitespaces. -/// - NSNumber -/// - Anything bigger than a `long long` or a `double` will be rejected. -/// - Unsigned values will be rejected. -/// - Booleans are supported, but should be initialized with `[NSNumber numberWithBool:]` or `@YES/@NO`. -/// - NSDate -/// - Note that since timezones are not supported, this will typically represent UTC dates. -/// - Using any unsupported type as a value (`NSNull, NSObject, NSArray, NSDictionary` for example) will **NOT** work. -/// - NSURL -/// - Must not be longer than 2048 characters. Can't be empty. -/// - Must follow the format `scheme://[authority][path][?query][#fragment]`. -/// - Parameters: -/// - attribute: The attribute value. If nil, the attribute will be removed. See method description for more info -/// about what's allowed. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -- (void)setAttribute:(nullable NSObject *)attribute forKey:(nonnull NSString *)key; - -/// Set a boolean custom user attribute for a key. -/// -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setBooleanAttribute:(BOOL)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set a date custom user attribute for a key. -/// -/// - Note: Since timezones are not supported, this will typically represent UTC dates. -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setDateAttribute:(nonnull NSDate *)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set a string custom user attribute for a key. -/// -/// Must not be longer than 64 characters. Can be empty. -/// For better results, you should make them upper/lowercase and trim the whitespaces. -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setStringAttribute:(nonnull NSString *)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set a boolean custom user attribute for a key. -/// -/// Anything bigger than a `long long` or a `double` will be rejected. -/// Unsigned values will be rejected. -/// Booleans are supported, but should be initialized with `[NSNumber numberWithBool:]` or `@YES/@NO`. -/// - Note: `setBoolean/Integer/LongLong/Float/Double` should be preferred over this method, especially for booleans. -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setNumberAttribute:(nonnull NSNumber *)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set an `NSInteger/Int` custom user attribute for a key. -/// -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setIntegerAttribute:(NSInteger)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set an `long long/Int64` custom user attribute for a key. -/// -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setLongLongAttribute:(long long)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set an float custom user attribute for a key. -/// -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setFloatAttribute:(float)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set an double custom user attribute for a key. -/// -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setDoubleAttribute:(double)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Set an url custom user attribute for a key. -/// -/// Must not be longer than 2048 characters. Can't be empty or nil. -/// Must follow the format `scheme://[authority][path][?query][#fragment]`. -/// - Parameters: -/// - attribute: The attribute value. -/// - key: The attribute key. Can't be nil. It should be made of letters, numbers or underscores (`[a-z0-9_]`) and -/// can't be longer than 30 characters. -/// - error: Pointer to an error describing. Note that the error is only about key/value validation, and doesn't -/// mean the value has been sent to the server yet. -/// - Returns: A boolean indicating whether the attribute passed validation or not. -- (BOOL)setURLAttribute:(nonnull NSURL *)attribute - forKey:(nonnull NSString *)key - error:(NSError *_Nullable *_Nullable)error NS_SWIFT_NAME(set(attribute:forKey:)); - -/// Removes an attribute for the specified key. -/// -/// - Parameter key: The attribute key. Can't be nil. -- (void)removeAttributeForKey:(nonnull NSString *)key; - -/// Removes all custom attributes. -/// -/// - Important: Once saved, this action cannot be undone. -- (void)clearAttributes; - -/// Add a tag to the specified collection. If empty, the collection will be created automatically. -/// -/// - Parameters: -/// - tag: The tag to add. Cannot be nil or empty. Must be a string no longer than 64 characters. -/// - collection: The tag collection name. Cannot be nil. Must be a string of letters, numbers or underscores -/// (`[a-z0-9_]`) and can't be longer than 30 characters. -- (void)addTag:(nonnull NSString *)tag inCollection:(nonnull NSString *)collection; - -/// Removes a tag from the specified collection. -/// -/// - Parameters: -/// - tag: Removes a tag from the specified collection. -/// - collection: The tag collection name. Cannot be nil. Must be a string of letters, numbers or underscores -/// (`[a-z0-9_]`) and can't be longer than 30 characters. If the collection doesn't exist, this method will do -/// nothing, but apply won't fail. -- (void)removeTag:(nonnull NSString *)tag fromCollection:(nonnull NSString *)collection; - -/// Removes all tags. -/// -/// - Important: Once saved, this action cannot be undone. -- (void)clearTags; - -/// Removes all tags from a collection. -/// -/// - Important: Once applied, this action cannot be undone. -/// - Parameter collection: The tag collection name. Cannot be nil. Must be a string of letters, numbers or underscores -/// (`[a-z0-9_]`) and can't be longer than 30 characters. -- (void)clearTagCollection:(nonnull NSString *)collection; - -/// Save all of the pending changes made in that editor. -/// -/// - Important:This action cannot be undone. -- (void)save; +/// Clear all tags and attributes set on an installation and their local cache returned by fetchAttributes and +/// fetchTagCollections. This doesn't affect data set on profiles using BatchProfile. ++ (void)clearInstallationData; @end diff --git a/Sources/Batch/BatchUser.m b/Sources/Batch/BatchUser.m index 84c8ec0..e7e9571 100644 --- a/Sources/Batch/BatchUser.m +++ b/Sources/Batch/BatchUser.m @@ -7,13 +7,13 @@ // #import +#import #import #import -#import #import #import #import -#import +#import #import #import #import @@ -24,18 +24,12 @@ NSString *const BatchEventTrackerFinishedWithSuccessKey = @"BatchEventTrackerFinishedWithSuccessKey"; -NSErrorDomain const BatchUserDataEditorErrorDomain = @"com.batch.ios.userdataeditor"; - @implementation BatchUser + (nullable NSString *)installationID { return [[BAPropertiesCenter valueForShortName:@"di"] uppercaseString]; } -+ (nonnull BatchUserDataEditor *)editor { - return [BatchUserDataEditor new]; -} - + (nonnull NSString *)language { NSString *savedLanguage = [[BAUserProfile defaultUserProfile] language]; return savedLanguage; @@ -107,210 +101,8 @@ + (void)fetchTagCollections:(void (^)(NSDictionary }); } -+ (void)trackEvent:(nonnull NSString *)event { - [BatchUser trackEvent:event withLabel:nil associatedData:nil]; -} - -+ (void)trackEvent:(nonnull NSString *)event withLabel:(nullable NSString *)label { - [BatchUser trackEvent:event withLabel:label associatedData:nil]; -} - -+ (void)trackEvent:(nonnull NSString *)event - withLabel:(nullable NSString *)label - data:(nullable NSDictionary *)legacyData { - BatchEventData *data = nil; - if (legacyData != nil) { - data = [BatchEventData new]; - [data _copyLegacyData:legacyData]; - } - - [BatchUser trackEvent:event withLabel:label associatedData:data]; -} - -+ (void)trackEvent:(nonnull NSString *)event - withLabel:(nullable NSString *)label - associatedData:(nullable BatchEventData *)data { - static id eventNameValidationRegexp = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSError *error = nil; - - eventNameValidationRegexp = [NSRegularExpression regularExpressionWithPattern:EVENT_NAME_REGEXP - options:0 - error:&error]; - if (error) { - // Something went really wrong, so we'll just throw internal errors - [BALogger errorForDomain:@"BatchUser - Events" message:@"Error while creating event name regexp."]; - eventNameValidationRegexp = nil; - } - }); - - if (!eventNameValidationRegexp) { - [BALogger publicForDomain:@"BatchUser - Events" message:@"Internal error. Ignoring attribute '%@'.", event]; - return; - } - - BOOL eventValidated = NO; - - if ([event isKindOfClass:[NSString class]]) { - NSRange matchingRange = [eventNameValidationRegexp rangeOfFirstMatchInString:event - options:0 - range:NSMakeRange(0, event.length)]; - if (matchingRange.location != NSNotFound) { - eventValidated = YES; - } - } - - if (!eventValidated) { - [BALogger publicForDomain:@"BatchUser - Events" message:@"Invalid event name ('%@'). Not tracking.", event]; - return; - } - - if (![label isKindOfClass:[NSString class]]) { - label = nil; - } - - if (![data isKindOfClass:[BatchEventData class]]) { - data = nil; - } - - [BATrackerCenter trackPublicEvent:event.uppercaseString label:label data:data]; -} - -+ (void)trackTransactionWithAmount:(double)amount { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [BatchUser trackTransactionWithAmount:amount data:nil]; -#pragma clang diagnostic pop -} - -+ (void)trackTransactionWithAmount:(double)amount data:(nullable NSDictionary *)legacyData { - NSMutableDictionary *params = [NSMutableDictionary new]; - - [params setObject:[NSNumber numberWithDouble:amount] forKey:BA_PUBLIC_EVENT_KEY_AMOUNT]; - - BatchEventData *data = nil; - if (legacyData != nil) { - data = [BatchEventData new]; - [data _copyLegacyData:legacyData]; - [params addEntriesFromDictionary:[data _internalDictionaryRepresentation]]; - } - - [BATrackerCenter trackPrivateEvent:@"T" parameters:params]; -} - -+ (void)trackLocation:(nonnull CLLocation *)location { - [BATrackerCenter trackLocation:location]; -} - -+ (void)printDebugInformation { - [BAUserDataManager printDebugInformation]; -} - -@end - -@implementation BatchUserDataEditor { - BAUserDataEditor *_backingImpl; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _backingImpl = [BAInjection injectClass:BAUserDataEditor.class]; - } - return self; -} - -- (void)setLanguage:(nullable NSString *)language { - [_backingImpl setLanguage:language]; -} - -- (void)setRegion:(nullable NSString *)region { - [_backingImpl setRegion:region]; -} - -- (void)setIdentifier:(nullable NSString *)identifier { - [_backingImpl setIdentifier:identifier]; -} - -- (void)setAttributionIdentifier:(nullable NSString *)attributionID { - [_backingImpl setAttributionIdentifier:attributionID]; -} - -- (BOOL)setEmail:(nullable NSString *)email error:(NSError **)error { - return [_backingImpl setEmail:email error:error]; -} - -- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state { - [_backingImpl setEmailMarketingSubscriptionState:state]; -} - -- (void)setAttribute:(nullable NSObject *)attribute forKey:(nonnull NSString *)key { - [_backingImpl setAttribute:attribute forKey:key]; -} - -- (BOOL)setBooleanAttribute:(BOOL)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setBooleanAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setDateAttribute:(NSDate *)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setDateAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setStringAttribute:(NSString *)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setStringAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setNumberAttribute:(NSNumber *)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setNumberAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setIntegerAttribute:(NSInteger)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setIntegerAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setLongLongAttribute:(long long int)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setLongLongAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setFloatAttribute:(float)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setFloatAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setDoubleAttribute:(double)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setDoubleAttribute:attribute forKey:key error:error]; -} - -- (BOOL)setURLAttribute:(nonnull NSURL *)attribute forKey:(NSString *)key error:(NSError **)error { - return [_backingImpl setURLAttribute:attribute forKey:key error:error]; -} - -- (void)removeAttributeForKey:(nonnull NSString *)key { - [_backingImpl removeAttributeForKey:key]; -} - -- (void)clearAttributes { - [_backingImpl clearAttributes]; -} - -- (void)addTag:(nonnull NSString *)tag inCollection:(nonnull NSString *)collection { - [_backingImpl addTag:tag inCollection:collection]; -} - -- (void)removeTag:(nonnull NSString *)tag fromCollection:(nonnull NSString *)collection { - [_backingImpl removeTag:tag fromCollection:collection]; -} - -- (void)clearTags { - [_backingImpl clearTags]; -} - -- (void)clearTagCollection:(nonnull NSString *)collection { - [_backingImpl clearTagCollection:collection]; -} - -- (void)save { - [_backingImpl save]; ++ (void)clearInstallationData { + [BAUserDataManager clearRemoteInstallationDataWithCompletion:nil]; } @end diff --git a/Sources/Batch/BatchUserProfile.h b/Sources/Batch/BatchUserProfile.h deleted file mode 100644 index f2ddc0f..0000000 --- a/Sources/Batch/BatchUserProfile.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// BatchUserProfile.h -// Batch -// -// https://batch.com -// Copyright (c) 2014 Batch SDK. All rights reserved. -// - -#import - -/// Describe a complete user profile. -/// Use this object to access all user info. -@interface BatchUserProfile : NSObject - -/// Set a custom user identifier to Batch, you should use this method if you have your own login system. -/// -/// - Warning: **Deprecated:** Please use ``Batch/BatchUser`` instead. -/// - Important: Be careful: Do not use it if you don't know what you are doing, giving a bad custom user ID can result -/// in failure of targeted push notifications delivery. -@property (strong, nonatomic) NSString *customIdentifier __attribute__((deprecated("Please use Batch User instead"))); - -/// The application language, default value is the device language. -/// Set to nil to reset to default value. -/// -/// - Warning: **Deprecated:** Please use ``Batch/BatchUser`` instead. -@property (strong, nonatomic) NSString *language __attribute__((deprecated("Please use Batch User instead"))); - -/// The application region, default value is the device region. -/// Set to nil to reset to default value. -/// -/// - Warning: **Deprecated:** Please use ``Batch/BatchUser`` instead. -@property (strong, nonatomic) NSString *region __attribute__((deprecated("Please use Batch User instead"))); - -@end diff --git a/Sources/Batch/BatchUserProfile.m b/Sources/Batch/BatchUserProfile.m deleted file mode 100644 index e3e86c8..0000000 --- a/Sources/Batch/BatchUserProfile.m +++ /dev/null @@ -1,77 +0,0 @@ -// -// BatchUserProfile.m -// Batch -// -// https://batch.com -// Copyright (c) 2014 Batch SDK. All rights reserved. -// - -#import - -#import -#import - -@interface BatchUserProfile () - -@property BAUserProfile *internal; - -@end - -@implementation BatchUserProfile - -#pragma mark - -#pragma mark Instance methods - -- (instancetype)init { - self = [super init]; - if ([BANullHelper isNull:self]) { - return self; - } - - _internal = [BAUserProfile defaultUserProfile]; - - return self; -} - -#pragma mark - -#pragma mark Properties override methods - -/*** Custom Identifier ***/ - -- (NSString *)customIdentifier { - return [self.internal customIdentifier]; -} - -- (void)setCustomIdentifier:(NSString *)customIdentifier { - [self.internal setCustomIdentifier:customIdentifier]; -} - -/*** Language ***/ - -- (NSString *)language { - NSString *value = [self.internal language]; - if (value == nil) { - value = [BAPropertiesCenter valueForShortName:@"dla"]; - } - return value; -} - -- (void)setLanguage:(NSString *)language { - [self.internal setLanguage:language]; -} - -/*** Region ***/ - -- (NSString *)region { - NSString *value = [self.internal region]; - if (value == nil) { - value = [BAPropertiesCenter valueForShortName:@"dre"]; - } - return value; -} - -- (void)setRegion:(NSString *)region { - [self.internal setRegion:region]; -} - -@end diff --git a/Sources/Batch/Defined.h b/Sources/Batch/Defined.h index d3f7195..773401f 100644 --- a/Sources/Batch/Defined.h +++ b/Sources/Batch/Defined.h @@ -8,9 +8,8 @@ #import -#include - #define ERROR_DOMAIN @"com.batch.ios" +#define PROFILE_ERROR_DOMAIN @"com.batch.ios.profile" #define NETWORKING_ERROR_DOMAIN @"com.batch.ios.networking" #define MESSAGING_ERROR_DOMAIN @"com.batch.ios.messaging" #define WEBVIEW_ERROR_DOMAIN @"com.batch.ios.webview" @@ -23,18 +22,6 @@ } \ *error = nil; -// BAVersion, BALevel and BAMessagingLevel have moved into Version.h and have been renamed -// Their values are not documented there because the Info.plist header parser is pretty dumb and will include comments -// If you need to get the user facing BAVersion, use BACoreCenter.sdkVersion - -// We need two intermedary function to make a quoted define from another define -// One expands the macro parameter -// The other adds the quotes -// It would be simpler to quote this in Versions.h but the plist preprocessor is dumb -#define BAStringifyValue(macro) #macro -#define BAGetStringifiedMacro(macro) BAStringifyValue(macro) -#define BASDKVersionNSString @BAGetStringifiedMacro(BASDKVersion) - #define BAPrivateKeyStorage @"Pm1oZKMo" #define BAPrivateKeyWebservice @"wgHD" #define BAPrivateKeyWebserviceV2 @"jgfx" @@ -46,27 +33,22 @@ #define kParametersReadReceiptEventName @"_PUSH_RECEIVED" // Application parameters. - +#define kParametersProjectKey @"app.project.key" #define kParametersLocalInstallIdentifierKey @"app.install.id" #define kParametersLocalInstallDateIdentifierKey @"app.install.timestamp" #define kParametersLocalIcloudIdentifierKey @"app.icloud.id" #define kParametersLocalIcloudDateIdentifierKey @"app.icloud.timestamp" -#define kParametersLocalServerInstallIdentifierKey @"app.server.id" #define kParametersPoolWebserviceMaxKey @"app.executor.maxpool" #define kParametersSystemCurrentAppVersionKey @"app.version.current" #define kParametersSystemPreviousAppVersionKey @"app.version.previous" -#define kParametersIDsPatternKey @"app.ids.pattern" -#define kParametersIDsPatternValue @"s,da,did,cus,idfa,dla,dre,dtz,osv,de,apv,apc,bid,pl,lvl,mlvl,pid,plv,brv" - -#define kParametersAdvancedIDsPatternKey @"app.ids.pattern_advanced" -#define kParametersAdvancedIDsPatternValue @"dty,sop" +#define kParametersSystemParameterPrefix @"app.system.param." +#define kParametersDataCollectionConfigKey @"app.data.collection.config." #define kParametersCustomUserIDKey @"app.id.custom" #define kParametersAppLanguageKey @"app.language" #define kParametersAppRegionKey @"app.region" -#define kParametersAttributionIDKey @"app.id.attribution" #define kParametersAppProfileVersionKey @"app.profile.version" @@ -84,13 +66,9 @@ // Latest notification authorization status sent to the server #define kParametersNotificationAuthSentStatusKey @"notification.auth.sentstatus" -#define kParametersAppNotificationSettingsKey @"notification.auth.appsettings" // Tracker parameters. -#define kParametersTrackerStateKey @"tracker.state" -#define kParametersTrackerStateValue @2 - #define kParametersTrackerDBLimitKey @"tracker.db.limit" #define kParametersTrackerDBLimitValue @10000 diff --git a/Sources/Batch/Kernel/Dependency Injection/BAInjection.h b/Sources/Batch/Kernel/Dependency Injection/BAInjection.h index 5cc1021..90b613f 100644 --- a/Sources/Batch/Kernel/Dependency Injection/BAInjection.h +++ b/Sources/Batch/Kernel/Dependency Injection/BAInjection.h @@ -53,8 +53,8 @@ // Injection methods -+ (nullable id)injectClass:(Class _Nonnull)classToInject; ++ (nullable id)injectClass:(Class _Nonnull)classToInject NS_REFINED_FOR_SWIFT; -+ (nullable id)injectProtocol:(nonnull Protocol *)protocolToInject; ++ (nullable id)injectProtocol:(nonnull Protocol *)protocolToInject NS_REFINED_FOR_SWIFT; @end diff --git a/Sources/Batch/Kernel/Dependency Injection/BAInjection.swift b/Sources/Batch/Kernel/Dependency Injection/BAInjection.swift new file mode 100644 index 0000000..bbfc62f --- /dev/null +++ b/Sources/Batch/Kernel/Dependency Injection/BAInjection.swift @@ -0,0 +1,29 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch.Batch_Private +import Foundation + +public extension BAInjection { + // Inject a Class or Protocol. Structs are not supported. + static func inject(_ requestedType: T.Type) -> T? { + // We have a single inject method as "T" matches Protocols in swift + // The only way to get "is" to work on a protocol is to force downcast it as AnyObject first + // as T.Type is actually an union. For example, calling `inject(EventTrackerProtocol)` makes + // requestedType a `EventTrackerProtocol & AnyObject` so we have to narrow it down first. + // Fortunately AnyObject can be casted as a Protocol so we can then give it to the appropriate + // Objective-C method! + if let requestedProtocol = requestedType as AnyObject as? Protocol { + return __inject(requestedProtocol) as? T + } + // Restricting the generic using `where T: AnyObject` or `` + // should make it so we do not need to do this dangerous cast and + // would prevent developers from injecting a Struct in this method. + // BUT, with Xcode 15.3 or lower it results in EXC_BAD_ACCESS for + // some reason, so we can't use that. Yay! + return __injectClass(requestedType as! AnyClass) as? T + } +} diff --git a/Sources/Batch/Kernel/Dependency Injection/Private/BAInjectionRegistrar.m b/Sources/Batch/Kernel/Dependency Injection/Private/BAInjectionRegistrar.m index 6971f9c..78c7e5f 100644 --- a/Sources/Batch/Kernel/Dependency Injection/Private/BAInjectionRegistrar.m +++ b/Sources/Batch/Kernel/Dependency Injection/Private/BAInjectionRegistrar.m @@ -5,23 +5,33 @@ // #import "BAInjectionRegistrar.h" +#import #import "BADisplayReceiptCenter.h" #import "BAEventDispatcherCenter.h" #import "BAInboxSQLiteDatasource.h" #import "BAInboxSQLiteHelper.h" #import "BAInjection.h" +#import "BAInstallDataEditor.h" #import "BALocalCampaignsFilePersistence.h" #import "BAMessagingAnalyticsDeduplicatingDelegate.h" #import "BAMessagingCenter.h" #import "BAMetricManager.h" #import "BAMetricRegistry.h" #import "BAPushSystemHelper.h" -#import "BAUserDataEditor.h" +#import "BATrackerCenter.h" #import "BAUserSQLiteDatasource.h" @implementation BAInjectionRegistrar + (void)registerInjectables { + // Register BAProfileCenter + [BAInjection registerInjectable:[BAInjectable injectableWithInstance:[BAProfileCenter sharedInstance]] + forProtocol:@protocol(BAProfileCenterProtocol)]; + + // Register BATEventTrackerProtocol + [BAInjection registerInjectable:[BAInjectable injectableWithInstance:[BATrackerCenter instance]] + forProtocol:@protocol(BATEventTrackerProtocol)]; + // Register BADisplayReceiptCenter [BAInjection registerInjectable:[BAInjectable injectableWithInstance:[BADisplayReceiptCenter new]] forClass:BADisplayReceiptCenter.class]; @@ -66,9 +76,15 @@ + (void)registerInjectables { // Register BAUserDataEditor [BAInjection registerInjectable:[BAInjectable injectableWithInitializer:^id() { - return [BAUserDataEditor new]; + return [BAInstallDataEditor new]; + }] + forClass:BAInstallDataEditor.class]; + + // Register BAProfileEditor + [BAInjection registerInjectable:[BAInjectable injectableWithInitializer:^id() { + return [BATProfileEditor new]; }] - forClass:BAUserDataEditor.class]; + forClass:BATProfileEditor.class]; // Register BAInboxSQLiteDatasource [BAInjection registerInjectable:[BAInjectable injectableWithInitializer:^id() { diff --git a/Sources/Batch/Kernel/Foundation/BATRegularExpression.swift b/Sources/Batch/Kernel/Foundation/BATRegularExpression.swift new file mode 100644 index 0000000..9c720b7 --- /dev/null +++ b/Sources/Batch/Kernel/Foundation/BATRegularExpression.swift @@ -0,0 +1,44 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +@objc +@objcMembers +/// Wrapper around BATRegularExpression reducing required boilerplate and common implementation errors +/// If the regexp fails to initialize, the class will not match anything on purpose as it's a very rare case. +/// Feel free to check the property to log if that happened. +public class BATRegularExpression: NSObject { + public private(set) var regexpFailedToInitialize: Bool = false + + private let backingRegexp: NSRegularExpression? + + public init(pattern: String, options: NSRegularExpression.Options = []) { + do { + backingRegexp = try NSRegularExpression(pattern: pattern, options: options) + } catch { + backingRegexp = nil + regexpFailedToInitialize = true + BALogger.public(domain: "Internal", message: "Failed to initialize regexp: \(error.localizedDescription)") + } + super.init() + } + + // Checks if the string fully matches the regexp + public func matches(_ target: String) -> Bool { + guard let backingRegexp else { + return false + } + + let fullStringRange = NSRange(location: 0, length: target.utf16.count) + let matchingRange = backingRegexp.rangeOfFirstMatch(in: target, range: fullStringRange) + + if matchingRange.location == NSNotFound || matchingRange != fullStringRange { + return false + } + return true + } +} diff --git a/Sources/Batch/Kernel/Foundation/BATSDKError.swift b/Sources/Batch/Kernel/Foundation/BATSDKError.swift new file mode 100644 index 0000000..b90e009 --- /dev/null +++ b/Sources/Batch/Kernel/Foundation/BATSDKError.swift @@ -0,0 +1,49 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +// A generic validation error used to surface a message to the developer +// Helps to enforce the good practice to have throwing methods with testable errors +// leaving choice to the caller on how to deal with this. +// Note: do not expose those errors to developers directly, have custom NSError wrappers +// or Obj-C code handling it to expose documented NSErrorDomains and code enums. +enum BATSDKError: Error { + case sdkInternal(subcode: Int, reason: String? = nil) + case userInputValidation(String) +} + +extension BATSDKError: CustomNSError { + public static var errorDomain: String = "BatchSDKError" + + public var errorCode: Int { + switch self { + case .sdkInternal: + return 1 + case .userInputValidation: + return 20 + } + } + + public var errorUserInfo: [String: Any] { + return [:] + } +} + +extension BATSDKError: LocalizedError { + public var errorDescription: String? { + switch self { + case let .sdkInternal(subcode, reason): + var description = "Internal error (code \(subcode))" + if let reason { + description += " Reason: \(reason)" + } + return description + case let .userInputValidation(reason): + return reason + } + } +} diff --git a/Sources/Batch/Kernel/Helpers/BADelegatedApplicationDelegate.m b/Sources/Batch/Kernel/Helpers/BADelegatedApplicationDelegate.m index 7d5dd45..fac5f87 100644 --- a/Sources/Batch/Kernel/Helpers/BADelegatedApplicationDelegate.m +++ b/Sources/Batch/Kernel/Helpers/BADelegatedApplicationDelegate.m @@ -125,11 +125,6 @@ - (void)swizzleMethodsOfDelegate:(nonnull id)delegate { // If you swizzle more methods, make sure that you add tests [self swizzle_didRegisterForRemoteNotificationsWithDeviceToken:class]; [self swizzle_didFailToRegisterForRemoteNotificationsWithError:class]; - [self swizzle_didReceiveRemoteNotification:class]; - [self swizzle_didReceiveRemoteNotification_fetchCompletionHandler:class]; - [self swizzle_didRegisterUserNotificationSettings:class]; - [self swizzle_handleActionWithIdentifier_forRemoteNotification_completionHandler:class]; - [self swizzle_handleActionWithIdentifier_forRemoteNotification_withResponseInfo_completionHandler:class]; } #pragma mark Swizzled method implementations @@ -170,112 +165,4 @@ - (void)swizzle_didFailToRegisterForRemoteNotificationsWithError:(Class)class { [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:false]; } -- (void)swizzle_didReceiveRemoteNotification:(Class)class { - BADelegatedApplicationDelegate *delegatedAppDelegate = self; - SEL selector = @selector(application:didReceiveRemoteNotification:); - id block = ^(id _self, UIApplication *application, NSDictionary *userInfo) { - [delegatedAppDelegate.batchDelegate application:application didReceiveRemoteNotification:userInfo]; - - IMP originalIMP = delegatedAppDelegate.original_didReceiveRemoteNotification; - if (originalIMP == NULL) { - return; - } - ((void (*)(id, SEL, UIApplication *, NSDictionary *))originalIMP)(_self, selector, application, userInfo); - }; - self.original_didReceiveRemoteNotification = - [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:false]; -} - -- (void)swizzle_didReceiveRemoteNotification_fetchCompletionHandler:(Class)class { - BADelegatedApplicationDelegate *delegatedAppDelegate = self; - SEL selector = @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:); - id block = ^(id _self, UIApplication *application, NSDictionary *userInfo, - void (^completionHandler)(UIBackgroundFetchResult result)) { - [delegatedAppDelegate.batchDelegate application:application - didReceiveRemoteNotification:userInfo - fetchCompletionHandler:completionHandler]; - - IMP originalIMP = delegatedAppDelegate.original_didReceiveRemoteNotification_fetchCompletionHandler; - if (originalIMP == NULL) { - return; - } - ((void (*)(id, SEL, UIApplication *, NSDictionary *, void (^)(UIBackgroundFetchResult result)))originalIMP)( - _self, selector, application, userInfo, completionHandler); - }; - self.original_didReceiveRemoteNotification_fetchCompletionHandler = - [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:true]; -} - -- (void)swizzle_didRegisterUserNotificationSettings:(Class)class { - BADelegatedApplicationDelegate *delegatedAppDelegate = self; - SEL selector = @selector(application:didRegisterUserNotificationSettings:); - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - - id block = ^(id _self, UIApplication *application, UIUserNotificationSettings *notificationSettings) { - [delegatedAppDelegate.batchDelegate application:application - didRegisterUserNotificationSettings:notificationSettings]; - - IMP originalIMP = delegatedAppDelegate.original_didRegisterUserNotificationSettings; - if (originalIMP == NULL) { - return; - } - ((void (*)(id, SEL, UIApplication *, UIUserNotificationSettings *))originalIMP)(_self, selector, application, - notificationSettings); - }; - -#pragma clang diagnostic pop - - self.original_didRegisterUserNotificationSettings = - [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:false]; -} - -- (void)swizzle_handleActionWithIdentifier_forRemoteNotification_completionHandler:(Class)class { - BADelegatedApplicationDelegate *delegatedAppDelegate = self; - SEL selector = @selector(application:handleActionWithIdentifier:forRemoteNotification:completionHandler:); - id block = ^(id _self, UIApplication *application, NSString *identifier, NSDictionary *userInfo, - void (^completionHandler)(void)) { - [delegatedAppDelegate.batchDelegate application:application - handleActionWithIdentifier:identifier - forRemoteNotification:userInfo - completionHandler:completionHandler]; - - IMP originalIMP = - delegatedAppDelegate.original_handleActionWithIdentifier_forRemoteNotification_completionHandler; - if (originalIMP == NULL) { - return; - } - ((void (*)(id, SEL, UIApplication *, NSString *, NSDictionary *, void (^)(void)))originalIMP)( - _self, selector, application, identifier, userInfo, completionHandler); - }; - self.original_handleActionWithIdentifier_forRemoteNotification_completionHandler = - [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:true]; -} - -- (void)swizzle_handleActionWithIdentifier_forRemoteNotification_withResponseInfo_completionHandler:(Class)class { - BADelegatedApplicationDelegate *delegatedAppDelegate = self; - SEL selector = @selector(application: - handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:); - id block = ^(id _self, UIApplication *application, NSString *identifier, NSDictionary *userInfo, - NSDictionary *responseInfo, void (^completionHandler)(void)) { - [delegatedAppDelegate.batchDelegate application:application - handleActionWithIdentifier:identifier - forRemoteNotification:userInfo - withResponseInfo:responseInfo - completionHandler:completionHandler]; - - IMP originalIMP = - delegatedAppDelegate - .original_handleActionWithIdentifier_forRemoteNotification_withResponseInfo_completionHandler; - if (originalIMP == NULL) { - return; - } - ((void (*)(id, SEL, UIApplication *, NSString *, NSDictionary *, NSDictionary *, void (^)(void)))originalIMP)( - _self, selector, application, identifier, userInfo, responseInfo, completionHandler); - }; - self.original_handleActionWithIdentifier_forRemoteNotification_withResponseInfo_completionHandler = - [self swizzleMethod:selector withBlock:block onClass:class skipIfTargetDoesntImplement:true]; -} - @end diff --git a/Sources/Batch/Kernel/Helpers/BAEmailUtils.h b/Sources/Batch/Kernel/Helpers/BAEmailUtils.h deleted file mode 100644 index 7a34aa8..0000000 --- a/Sources/Batch/Kernel/Helpers/BAEmailUtils.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// Batch -// -// Copyright © Batch.com. All rights reserved. -// - -#import - -/// Email max length -#define EMAIL_MAX_LENGTH 128 - -NS_ASSUME_NONNULL_BEGIN - -@interface BAEmailUtils : NSObject - -/// Check wether email is valid -+ (BOOL)isValidEmail:(nonnull NSString *)email; - -/// Check wether email is too long -+ (BOOL)isEmailTooLong:(nonnull NSString *)email; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Batch/Kernel/Helpers/BAEmailUtils.m b/Sources/Batch/Kernel/Helpers/BAEmailUtils.m deleted file mode 100644 index 6e14b34..0000000 --- a/Sources/Batch/Kernel/Helpers/BAEmailUtils.m +++ /dev/null @@ -1,28 +0,0 @@ -// -// Batch -// -// Copyright © Batch.com. All rights reserved. -// - -#import "BAEmailUtils.h" - -/// Email regexp -#define EMAIL_VALUE_RULE @"^(\\S+@\\S+\\.\\S+)$" - -@implementation BAEmailUtils - -+ (BOOL)isValidEmail:(nonnull NSString *)email { - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:EMAIL_VALUE_RULE - options:0 - error:nil]; - NSRange matchingRange = [regex rangeOfFirstMatchInString:email options:0 range:NSMakeRange(0, email.length)]; - if (matchingRange.location == NSNotFound) { - return false; - } - return true; -} - -+ (BOOL)isEmailTooLong:(NSString *)email { - return [email length] > EMAIL_MAX_LENGTH; -} -@end diff --git a/Sources/Batch/Kernel/Helpers/BAPartialApplicationDelegate.h b/Sources/Batch/Kernel/Helpers/BAPartialApplicationDelegate.h index 2f81d13..806473c 100644 --- a/Sources/Batch/Kernel/Helpers/BAPartialApplicationDelegate.h +++ b/Sources/Batch/Kernel/Helpers/BAPartialApplicationDelegate.h @@ -18,31 +18,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo; - -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (void)application:(UIApplication *)application - didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; - -#pragma clang diagnostic pop - -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(nullable NSString *)identifier - forRemoteNotification:(NSDictionary *)userInfo - completionHandler:(void (^)(void))completionHandler; - -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(nullable NSString *)identifier - forRemoteNotification:(NSDictionary *)userInfo - withResponseInfo:(NSDictionary *)responseInfo - completionHandler:(void (^)(void))completionHandler; - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Batch/Kernel/Helpers/BAWindowHelper.h b/Sources/Batch/Kernel/Helpers/BAWindowHelper.h index 8a2d673..229e4a9 100644 --- a/Sources/Batch/Kernel/Helpers/BAWindowHelper.h +++ b/Sources/Batch/Kernel/Helpers/BAWindowHelper.h @@ -11,9 +11,9 @@ NS_ASSUME_NONNULL_BEGIN @interface BAWindowHelper : NSObject -+ (nullable UIWindowScene *)activeScene NS_AVAILABLE_IOS(13.0); ++ (nullable UIWindowScene *)activeScene; -+ (nullable UIWindowScene *)activeWindowScene NS_AVAILABLE_IOS(13.0); ++ (nullable UIWindowScene *)activeWindowScene; + (nullable UIWindow *)keyWindow; diff --git a/Sources/Batch/Kernel/Helpers/BAWindowHelper.m b/Sources/Batch/Kernel/Helpers/BAWindowHelper.m index 6beef3a..1dc33af 100644 --- a/Sources/Batch/Kernel/Helpers/BAWindowHelper.m +++ b/Sources/Batch/Kernel/Helpers/BAWindowHelper.m @@ -9,7 +9,7 @@ @implementation BAWindowHelper -+ (nullable UIWindowScene *)activeScene NS_AVAILABLE_IOS(13.0) { ++ (nullable UIWindowScene *)activeScene { NSSet *connectedScenes = UIApplication.sharedApplication.connectedScenes; for (UIScene *scene in connectedScenes) { if (scene.activationState == UISceneActivationStateForegroundActive) { @@ -19,7 +19,7 @@ + (nullable UIWindowScene *)activeScene NS_AVAILABLE_IOS(13.0) { return nil; } -+ (nullable UIWindowScene *)activeWindowScene NS_AVAILABLE_IOS(13.0) { ++ (nullable UIWindowScene *)activeWindowScene { NSSet *connectedScenes = UIApplication.sharedApplication.connectedScenes; for (UIScene *scene in connectedScenes) { if (scene.activationState == UISceneActivationStateForegroundActive && @@ -31,15 +31,23 @@ + (nullable UIWindowScene *)activeWindowScene NS_AVAILABLE_IOS(13.0) { } + (UIWindow *)keyWindow { - if (@available(iOS 13.0, *)) { - UIWindow *window = [self keyWindowFromSceneAPI]; - return window != nil ? window : UIApplication.sharedApplication.keyWindow; - } else { - return UIApplication.sharedApplication.keyWindow; - } + UIWindow *window = [self keyWindowFromSceneAPI]; + +#if TARGET_OS_VISION + + return window; + +#else + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return window != nil ? window : UIApplication.sharedApplication.keyWindow; +#pragma clang diagnostic pop + +#endif } -+ (nullable UIWindow *)keyWindowFromSceneAPI NS_AVAILABLE_IOS(13.0) { ++ (nullable UIWindow *)keyWindowFromSceneAPI { // Don't use activeWindowScene as we want to loop on all scenes until we get what we want NSSet *connectedScenes = UIApplication.sharedApplication.connectedScenes; for (UIScene *scene in connectedScenes) { diff --git a/Sources/Batch/Kernel/Logger/BALogger.m b/Sources/Batch/Kernel/Logger/BALogger.m index 98af601..a5f5d45 100644 --- a/Sources/Batch/Kernel/Logger/BALogger.m +++ b/Sources/Batch/Kernel/Logger/BALogger.m @@ -97,7 +97,7 @@ + (void)__SWIFT_warningForDomain:(nullable NSString *)domain message:(NSString * } + (void)__SWIFT_debugForDomain:(nullable NSString *)domain message:(NSString *)message { - [self warningForDomain:domain message:@"%@", message]; + [self debugForDomain:domain message:@"%@", message]; } #pragma mark - Internal log control diff --git a/Sources/Batch/Kernel/Parameters/BANetworkParameters.h b/Sources/Batch/Kernel/Parameters/BANetworkParameters.h deleted file mode 100644 index eb7c89d..0000000 --- a/Sources/Batch/Kernel/Parameters/BANetworkParameters.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// BANetworkParameters.h -// Batch -// -// https://batch.com -// Copyright (c) 2012 Batch SDK. All rights reserved. -// - -#import - -@interface BANetworkParameters : NSObject - -+ (NSString *)simOperatorCode; - -@end diff --git a/Sources/Batch/Kernel/Parameters/BANetworkParameters.m b/Sources/Batch/Kernel/Parameters/BANetworkParameters.m deleted file mode 100644 index 4ea83f4..0000000 --- a/Sources/Batch/Kernel/Parameters/BANetworkParameters.m +++ /dev/null @@ -1,84 +0,0 @@ -// -// BANetworkParameters.m -// Batch -// -// https://batch.com -// Copyright (c) 2012 Batch SDK. All rights reserved. -// - -#import - -#if TARGET_OS_MACCATALYST - -@implementation BANetworkParameters - -// MCC+MNC -+ (NSString *)simOperatorCode { - return @""; -} - -@end - -#else - -#import -#import -#import -#import - -@implementation BANetworkParameters - -// MCC+MNC -+ (NSString *)simOperatorCode { - if (@available(iOS 16.4, *)) { - // iOS 16.4 removed support for CTCarrier and returns garbage - return nil; - } else { - static CTTelephonyNetworkInfo *telephonyNetworkInfo = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - telephonyNetworkInfo = [[CTTelephonyNetworkInfo alloc] init]; - }); - // TODO add cache - CTCarrier *carrier; - - if (@available(iOS 12.1, *)) { - // This APi is available from iOS 12.0, but returns nil - NSDictionary *carriers = [telephonyNetworkInfo serviceSubscriberCellularProviders]; - for (CTCarrier *candidateCarrier in carriers.allValues) { - // Try to find a SIM that's active. - // Note that for dual sim both can be active. We don't know which one is the default, so we just pick - // one. - if ([candidateCarrier.mobileCountryCode length] > 0 || - [candidateCarrier.mobileNetworkCode length] > 0) { - carrier = candidateCarrier; - break; - } - } - } else { - carrier = [telephonyNetworkInfo subscriberCellularProvider]; - } - - NSString *mobileCountryCode = [carrier mobileCountryCode]; - NSString *mobileNetworkCode = [carrier mobileNetworkCode]; - - if (mobileCountryCode == nil) { - mobileCountryCode = @""; - } - - if (mobileNetworkCode == nil) { - mobileNetworkCode = @""; - } - - NSString *operatorCode = [mobileCountryCode stringByAppendingString:mobileNetworkCode]; - if ([operatorCode length] > 0) { - return operatorCode; - } else { - return nil; - } - } -} - -@end - -#endif diff --git a/Sources/Batch/Kernel/Parameters/BAPropertiesCenter.m b/Sources/Batch/Kernel/Parameters/BAPropertiesCenter.m index 1966e2f..3635482 100644 --- a/Sources/Batch/Kernel/Parameters/BAPropertiesCenter.m +++ b/Sources/Batch/Kernel/Parameters/BAPropertiesCenter.m @@ -7,7 +7,6 @@ #import -#import #import #import @@ -17,6 +16,7 @@ #import #import #import +#import #import @@ -68,7 +68,6 @@ - (void)setupParameterMappings { @"di" : @"localInstallation", @"cus" : @"customIdentifier", @"tok" : @"pushToken", - @"idfa" : @"attributionID", @"dre" : @"deviceRegion", @"dla" : @"deviceLanguage", @"dtz" : @"deviceTimezone", @@ -78,10 +77,6 @@ - (void)setupParameterMappings { @"did" : @"sdkInstallDate", @"dty" : @"deviceType", @"osv" : @"deviceOSVersion", - @"de" : @"density", - @"sw" : @"screenWidth", - @"sh" : @"screenHeight", - @"so" : @"screenOrientation", @"bid" : @"bundleID", @"pid" : @"applicationID", @"pl" : @"platform", @@ -92,7 +87,6 @@ - (void)setupParameterMappings { @"plv" : @"pluginVersion", @"brv" : @"bridgeVersion", @"nty" : @"notifType", - @"sop" : @"simOperatorCode", @"s" : @"sessionIdentifier" }; } @@ -160,15 +154,6 @@ - (NSString *)pushToken { return nil; } -- (NSString *)attributionID { - return [BAParameter objectForKey:kParametersAttributionIDKey fallback:nil]; -} - -// MCC+MNC -- (NSString *)simOperatorCode { - return [BANetworkParameters simOperatorCode]; -} - - (NSString *)deviceRegion { return [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode]; } @@ -211,8 +196,10 @@ - (NSString *)sdkInstallDate { } - (NSString *)deviceType { -#if TARGET_OS_MACCATALYST - return [@"Mac - " stringByAppendingString:[BAOSHelper deviceCode]]; +#if TARGET_OS_VISION + return [@"visionOS - " stringByAppendingString:[BAOSHelper deviceCode]]; +#elif TARGET_OS_MACCATALYST + return [@"macOS - " stringByAppendingString:[BAOSHelper deviceCode]]; #elif TARGET_OS_SIMULATOR return [@"Simulator - " stringByAppendingString:[BAOSHelper deviceCode]]; #else @@ -224,23 +211,6 @@ - (NSString *)deviceOSVersion { return [[UIDevice currentDevice] systemVersion]; } -- (NSString *)density { - return [NSString stringWithFormat:@"%.1f", [[UIScreen mainScreen] scale]]; -} - -- (NSString *)screenWidth { - return [NSString stringWithFormat:@"%.1f", [[UIScreen mainScreen] bounds].size.width]; -} - -- (NSString *)screenHeight { - return [NSString stringWithFormat:@"%.1f", [[UIScreen mainScreen] bounds].size.height]; -} - -- (NSString *)screenOrientation { - BOOL isPortrait = UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]); - return [NSString stringWithFormat:@"%@", isPortrait ? @"P" : @"L"]; -} - - (NSString *)bundleID { return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; } @@ -349,6 +319,9 @@ - (NSString *)notifTypeFallback { // Might not give the best result (false positives) __block int types = 0; + // No fallback on visionOS. I wish we could get rid of this codepath altogether but we can't. +#if !TARGET_OS_VISION + [BAThreading performBlockOnMainThread:^{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -356,6 +329,8 @@ - (NSString *)notifTypeFallback { #pragma clang diagnostic pop }]; +#endif + return [NSString stringWithFormat:@"%i", types]; } diff --git a/Sources/Batch/Kernel/Parameters/BAUserDefaults.h b/Sources/Batch/Kernel/Parameters/BAUserDefaults.h index 50f136e..e5fda98 100644 --- a/Sources/Batch/Kernel/Parameters/BAUserDefaults.h +++ b/Sources/Batch/Kernel/Parameters/BAUserDefaults.h @@ -58,22 +58,6 @@ */ - (void)removeObjectForKey:(NSString *_Nonnull)key; -/*! - @method saveCustomObject:key: - @abstract Save a custom object implementing NSCoding. - @param object : An object implementing NSCoding - @param key : Unique key to same this object on. - */ -- (void)saveCustomObject:(id _Nonnull)object key:(NSString *_Nonnull)key; - -/*! - @method loadCustomObjectWithKey: - @abstract Load a custom object implementing NSCoding. - @param key : Stored key. - @return The found object or NULL. - */ -- (id _Nullable)loadCustomObjectWithKey:(NSString *_Nonnull)key; - /** Remove all k/v */ diff --git a/Sources/Batch/Kernel/Parameters/BAUserDefaults.m b/Sources/Batch/Kernel/Parameters/BAUserDefaults.m index ee28fd5..42e06ec 100644 --- a/Sources/Batch/Kernel/Parameters/BAUserDefaults.m +++ b/Sources/Batch/Kernel/Parameters/BAUserDefaults.m @@ -89,21 +89,6 @@ - (void)removeObjectForKey:(NSString *)key { [_defaults synchronize]; } -// Save a custom object implementing NSCoding. -- (void)saveCustomObject:(id)object key:(NSString *)key { - NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object]; - [_defaults setObject:encodedObject forKey:key]; - [_defaults synchronize]; -} - -// Load a custom object implementing NSCoding. -- (id)loadCustomObjectWithKey:(NSString *)key { - NSData *encodedObject = [_defaults objectForKey:key]; - id object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject]; - - return object; -} - - (void)removeAllObjects { for (NSString *key in _defaults.dictionaryRepresentation.keyEnumerator) { [_defaults removeObjectForKey:key]; diff --git a/Sources/Batch/Modules/Actions/BAActionsCenter.m b/Sources/Batch/Modules/Actions/BAActionsCenter.m index 085078b..52dea6c 100644 --- a/Sources/Batch/Modules/Actions/BAActionsCenter.m +++ b/Sources/Batch/Modules/Actions/BAActionsCenter.m @@ -69,7 +69,9 @@ - (instancetype)init { } - (void)registerInternalAction:(nonnull BatchUserAction *)action { - [registeredActions setObject:action forKey:action.identifier]; + if (action != nil) { + [registeredActions setObject:action forKey:action.identifier]; + } } - (NSError *)registerAction:(nonnull BatchUserAction *)action { @@ -284,6 +286,10 @@ - (BatchUserAction *)clipboardAction { * Action that triggers the rating prompt */ - (BatchUserAction *)ratingAction { +#if TARGET_OS_VISION + // There is no way (yet) to request a rating on visionOS + return nil; +#else return [BatchUserAction userActionWithIdentifier:[kBAActionsReservedIdentifierPrefix stringByAppendingString:@"rating"] actionBlock:^(NSString *_Nonnull identifier, @@ -298,6 +304,7 @@ - (BatchUserAction *)ratingAction { } [SKStoreReviewController requestReview]; }]; +#endif } - (void)performGroupAction:(nonnull NSDictionary *)rawArguments diff --git a/Sources/Batch/Modules/Actions/BAUserDataBuiltinActions.m b/Sources/Batch/Modules/Actions/BAUserDataBuiltinActions.m index cc34902..132e4b8 100644 --- a/Sources/Batch/Modules/Actions/BAUserDataBuiltinActions.m +++ b/Sources/Batch/Modules/Actions/BAUserDataBuiltinActions.m @@ -61,14 +61,17 @@ + (void)performTagEdit:(NSDictionary *_Nonnull)arguments action = [action lowercaseString]; if ([@"add" isEqualToString:action]) { - [BALogger debugForDomain:LOCAL_LOG_DOMAIN message:@"Adding tag '%@' to collection '%@'", tag, collection]; - BatchUserDataEditor *editor = [BatchUser editor]; - [editor addTag:tag inCollection:collection]; + [BALogger debugForDomain:LOCAL_LOG_DOMAIN + message:@"Adding string item '%@' to array attribute '%@'", tag, collection]; + + BatchProfileEditor *editor = [BatchProfile editor]; + [editor addItemToStringArrayAttribute:tag forKey:collection error:nil]; [editor save]; } else if ([@"remove" isEqualToString:action]) { - [BALogger debugForDomain:LOCAL_LOG_DOMAIN message:@"Removing tag '%@' from collection '%@'", tag, collection]; - BatchUserDataEditor *editor = [BatchUser editor]; - [editor removeTag:tag fromCollection:collection]; + [BALogger debugForDomain:LOCAL_LOG_DOMAIN + message:@"Removing string item '%@' from array attribute '%@'", tag, collection]; + BatchProfileEditor *editor = [BatchProfile editor]; + [editor removeItemFromStringArrayAttribute:tag forKey:collection error:nil]; [editor save]; } else { [BALogger debugForDomain:LOCAL_LOG_DOMAIN diff --git a/Sources/Batch/Modules/Actions/BAUserEventBuiltinActions.m b/Sources/Batch/Modules/Actions/BAUserEventBuiltinActions.m index 0c89b3e..d208759 100644 --- a/Sources/Batch/Modules/Actions/BAUserEventBuiltinActions.m +++ b/Sources/Batch/Modules/Actions/BAUserEventBuiltinActions.m @@ -2,6 +2,7 @@ #import #import #import +#import #import #define LOCAL_LOG_DOMAIN @"BatchActions" @@ -36,20 +37,28 @@ + (void)performTrackEvent:(NSDictionary *_Nonnull)argume return; } - BatchEventData *data = [BatchEventData new]; + BatchEventAttributes *attributes = [BatchEventAttributes new]; NSString *label = [json objectForKey:@"l" kindOfClass:[NSString class] allowNil:YES error:&err]; + if ([label stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length > 0) { + [attributes putString:label forKey:@"$label"]; + } NSArray *tags = [json objectForKey:@"t" kindOfClass:[NSArray class] allowNil:YES error:&err]; if ([tags count] > 0) { + NSMutableArray *safeTags = [NSMutableArray arrayWithCapacity:tags.count]; for (NSObject *tagValue in tags) { - if ([tagValue isKindOfClass:[NSString class]]) { - [data addTag:(NSString *)tagValue]; + if ([tagValue isKindOfClass:[NSString class]] && ((NSString *)tagValue).length > 0) { + [safeTags addObject:tagValue]; } else { [BALogger debugForDomain:LOCAL_LOG_DOMAIN message:@"Could not add tag in track event action: invalid class '%@'", NSStringFromClass([tagValue class])]; } } + + if (safeTags.count > 0) { + [attributes putStringArray:safeTags forKey:@"$tags"]; + } } NSDictionary *argsData = [json objectForKey:@"a" kindOfClass:[NSDictionary class] allowNil:YES error:&err]; @@ -61,9 +70,9 @@ + (void)performTrackEvent:(NSDictionary *_Nonnull)argume if ([argValue isKindOfClass:[NSString class]]) { NSDate *dateValue = [self parseISO8601:(NSString *)argValue]; if (dateValue != nil) { - [data putDate:dateValue forKey:key]; + [attributes putDate:dateValue forKey:key]; } else { - [data putString:(NSString *)argValue forKey:key]; + [attributes putString:(NSString *)argValue forKey:key]; } } else if ([argValue isKindOfClass:[NSNumber class]]) { NSNumber *numberAttr = (NSNumber *)argValue; @@ -77,23 +86,23 @@ + (void)performTrackEvent:(NSDictionary *_Nonnull)argume message:@"Args data for key '%@' is a NSNumber: %s", key, ctype]; if (numberAttr == (void *)kCFBooleanFalse || (NSNumber *)numberAttr == (void *)kCFBooleanTrue) { // Boolean value - [data putBool:[numberAttr boolValue] forKey:key]; + [attributes putBool:[numberAttr boolValue] forKey:key]; } else if (strcmp(ctype, @encode(char)) == 0 || strcmp(ctype, @encode(short)) == 0 || strcmp(ctype, @encode(int)) == 0 || strcmp(ctype, @encode(long)) == 0 || strcmp(ctype, @encode(long long)) == 0) { // Long long might be truncated on 32 bit platforms - [data putInteger:[numberAttr integerValue] forKey:key]; + [attributes putInteger:[numberAttr integerValue] forKey:key]; } else if (strcmp(ctype, @encode(float)) == 0 || strcmp(ctype, @encode(double)) == 0) { // Decimal values - [data putDouble:[numberAttr doubleValue] forKey:key]; + [attributes putDouble:[numberAttr doubleValue] forKey:key]; } else if (strcmp(ctype, @encode(BOOL)) == 0) { // According to the documentation that's not supported, but give it a shot - [data putBool:[numberAttr boolValue] forKey:key]; + [attributes putBool:[numberAttr boolValue] forKey:key]; } else { // Try to make it work in a NSInteger NSInteger val = [numberAttr integerValue]; if ([numberAttr isEqualToNumber:[NSNumber numberWithInteger:val]]) { - [data putInteger:[numberAttr integerValue] forKey:key]; + [attributes putInteger:[numberAttr integerValue] forKey:key]; } } } else { @@ -104,7 +113,7 @@ + (void)performTrackEvent:(NSDictionary *_Nonnull)argume } } - [BatchUser trackEvent:event withLabel:label associatedData:data]; + [BatchProfile trackEventWithName:event attributes:attributes]; } + (NSDate *)parseISO8601:(NSString *)dateString { diff --git a/Sources/Batch/Modules/Core/BAApplicationLifecycle.m b/Sources/Batch/Modules/Core/BAApplicationLifecycle.m index 8a45486..2ef3b9b 100644 --- a/Sources/Batch/Modules/Core/BAApplicationLifecycle.m +++ b/Sources/Batch/Modules/Core/BAApplicationLifecycle.m @@ -12,23 +12,17 @@ @implementation BAApplicationLifecycle + (BOOL)applicationUsesSwiftUILifecycle { - if (@available(iOS 13.0, *)) { - UIScene *scene = UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; - id delegate = scene.delegate; - if (delegate == nil) { - return false; - } - // Expected name is SwiftUI.AppSceneDelegate but we expect it to change - return [NSStringFromClass([delegate class]) hasPrefix:@"SwiftUI."]; + UIScene *scene = UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; + id delegate = scene.delegate; + if (delegate == nil) { + return false; } - return false; + // Expected name is SwiftUI.AppSceneDelegate but we expect it to change + return [NSStringFromClass([delegate class]) hasPrefix:@"SwiftUI."]; } + (BOOL)applicationUsesUIScene { - if (@available(iOS 13.0, *)) { - return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"] != nil; - } - return false; + return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"] != nil; } + (BOOL)applicationImplementsUNDelegate { @@ -36,10 +30,8 @@ + (BOOL)applicationImplementsUNDelegate { } + (BOOL)isApplicationUIOnScreen { - if (@available(iOS 13.0, *)) { - if ([self hasASceneInState:UISceneActivationStateForegroundActive]) { - return true; - } + if ([self hasASceneInState:UISceneActivationStateForegroundActive]) { + return true; } return UIApplication.sharedApplication.applicationState == UIApplicationStateActive; } @@ -57,14 +49,12 @@ + (BOOL)hasASceneInState:(UISceneActivationState)activationState { } + (BOOL)hasASceneInForegroundState { - if (@available(iOS 13.0, *)) { - if ([self applicationUsesUIScene]) { - NSSet *connectedScenes = UIApplication.sharedApplication.connectedScenes; - for (UIScene *scene in connectedScenes) { - if (scene.activationState == UISceneActivationStateForegroundActive || - scene.activationState == UISceneActivationStateForegroundInactive) { - return true; - } + if ([self applicationUsesUIScene]) { + NSSet *connectedScenes = UIApplication.sharedApplication.connectedScenes; + for (UIScene *scene in connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive || + scene.activationState == UISceneActivationStateForegroundInactive) { + return true; } } } diff --git a/Sources/Batch/Modules/Core/BACenterMulticastDelegate.h b/Sources/Batch/Modules/Core/BACenterMulticastDelegate.h index b531652..2aa0144 100644 --- a/Sources/Batch/Modules/Core/BACenterMulticastDelegate.h +++ b/Sources/Batch/Modules/Core/BACenterMulticastDelegate.h @@ -29,34 +29,6 @@ */ + (void)batchDidStart; -/*! - @method handleURL: - @abstract Give the URL to Batch systems. - @discussion Call this method in application:openURL:sourceApplication:annotation: of your UIApplicationDelegate - @discussion You can call this method from any thread. - @param url : The input URL. - @return YES if Batch take care of this URL, No otherwise. - @warning The delegate methods is always called in the main thread! - */ -+ (BOOL)handleURL:(NSURL *)url __attribute__((warn_unused_result)); - -/*! - @method setCustomUserIdentifier: - @abstract Set the custom user identifier to Batch, you should use this method if you have your own login system. - @discussion You can call this method from any thread. - @param identifier : The unique user identifier. - @warning Be carefull: Do not use it if you don't know what you are doing, giving a bad custom user ID can result in - failure into offer delivery and restore. - */ -+ (void)setCustomUserIdentifier:(NSString *)identifier; - -/*! - @method setUseAdvancedDeviceInformation: - @abstract Set if Batch can try to use advanced device information (default = YES) - @warning You should only use it if you know what you are doing. - */ -+ (void)setUseAdvancedDeviceInformation:(BOOL)use; - @end /*! diff --git a/Sources/Batch/Modules/Core/BACenterMulticastDelegate.m b/Sources/Batch/Modules/Core/BACenterMulticastDelegate.m index 47af604..41faae2 100644 --- a/Sources/Batch/Modules/Core/BACenterMulticastDelegate.m +++ b/Sources/Batch/Modules/Core/BACenterMulticastDelegate.m @@ -18,6 +18,7 @@ #import #import #import +#import @implementation BACenterMulticastDelegate @@ -30,7 +31,8 @@ + (void)initialize { kPluginsList = @[ [BACoreCenter class], [BAPushCenter class], [BATrackerCenter class], [BAUserCenter class], [BAMessagingCenter class], [BAActionsCenter class], [BALocalCampaignsCenter class], - [BAEventDispatcherCenter class], [BADisplayReceiptCenter class] + [BAEventDispatcherCenter class], [BADisplayReceiptCenter class], [BAProfileCenter class], + [BATDataCollectionCenter class] ]; } @@ -67,39 +69,4 @@ + (void)startWithAPIKey:(NSString *)key { } } } - -// Give the URL to Batch systems. -+ (BOOL)handleURL:(NSURL *)url { - if (!url) { - return NO; - } - - BOOL hasHandled = NO; - - for (id plugin in kPluginsList) { - if ([plugin respondsToSelector:@selector(handleURL:)]) { - hasHandled |= [plugin handleURL:url]; - } - } - - return hasHandled; -} - -// Set the custom user identifier to Batch, you should use this method if you have your own login system. -+ (void)setCustomUserIdentifier:(NSString *)identifier { - for (id plugin in kPluginsList) { - if ([plugin respondsToSelector:@selector(setCustomUserIdentifier:)]) { - [plugin setCustomUserIdentifier:identifier]; - } - } -} - -+ (void)setUseAdvancedDeviceInformation:(BOOL)use { - for (id plugin in kPluginsList) { - if ([plugin respondsToSelector:@selector(setUseAdvancedDeviceInformation:)]) { - [plugin setUseAdvancedDeviceInformation:use]; - } - } -} - @end diff --git a/Sources/Batch/Modules/Core/BAConfiguration.h b/Sources/Batch/Modules/Core/BAConfiguration.h index 7998d50..9a2ac43 100644 --- a/Sources/Batch/Modules/Core/BAConfiguration.h +++ b/Sources/Batch/Modules/Core/BAConfiguration.h @@ -21,20 +21,6 @@ extern NSString *_Nonnull const kBATConfigurationChangedNotification; */ @interface BAConfiguration : NSObject -/*! - @method setUseAdvancedDeviceInformation: - @abstract Set the advanced device information use condition. - @param use : YES to allow use of advanced device information, NO otherwise. - */ -- (void)setUseAdvancedDeviceInformation:(BOOL)use; - -/*! - @method useAdvancedDeviceInformation - @abstract Condition to use the advanced device information. - @return YES if Batch can use advanced device information, NO otherwise. - */ -- (BOOL)useAdvancedDeviceInformation __attribute__((warn_unused_result)); - /*! @method setDevelopperKey: @abstract Keep and check the developper key value. @@ -50,13 +36,6 @@ extern NSString *_Nonnull const kBATConfigurationChangedNotification; */ - (nullable NSString *)developperKey __attribute__((warn_unused_result)); -/*! - @method developmentMode - @abstract Get the development mode. - @return YES if in develpment mode, NO for release. - */ -- (BOOL)developmentMode __attribute__((warn_unused_result)); - /*! @method setLoggerDelegate: @abstract Change the logger delegate. @@ -88,4 +67,16 @@ extern NSString *_Nonnull const kBATConfigurationChangedNotification; */ @property (weak, nonatomic, nullable) id deeplinkDelegate; +/*! + @method setDisabledMigrations + @abstract Set profile's data migration to disable + */ +- (void)setDisabledMigrations:(BatchMigration)migrations; + +/*! + @method isMigrationDisabledFor + @abstract Whether the given migration is disabled + */ +- (Boolean)isMigrationDisabledFor:(BatchMigration)migration; + @end diff --git a/Sources/Batch/Modules/Core/BAConfiguration.m b/Sources/Batch/Modules/Core/BAConfiguration.m index 6133957..dfa72fd 100644 --- a/Sources/Batch/Modules/Core/BAConfiguration.m +++ b/Sources/Batch/Modules/Core/BAConfiguration.m @@ -22,14 +22,14 @@ @interface BAConfiguration () { // Application developper key. NSString *_developperKey; - // Use advanced device information. - BOOL _advancedDeviceInformation; - // Logger delegate. __weak id _loggerDelegate; // Universal links associated domains NSMutableArray *_associatedDomains; + + // Migrations related configuration + BatchMigration _disabledMigrations; } @end @@ -41,25 +41,12 @@ - (instancetype)init { if ([BANullHelper isNull:self] == YES) { return nil; } - - // We allow advanced device information (aka advenced identifiers) by default - _advancedDeviceInformation = YES; - return self; } #pragma mark - #pragma mark Public methods -- (void)setUseAdvancedDeviceInformation:(BOOL)use { - _advancedDeviceInformation = use; - [[BANotificationCenter defaultCenter] postNotificationName:kBATConfigurationChangedNotification object:nil]; -} - -- (BOOL)useAdvancedDeviceInformation { - return _advancedDeviceInformation; -} - // Keep and check the developper key value. - (NSError *)setDevelopperKey:(NSString *)key { // Check developper key. @@ -88,11 +75,6 @@ - (NSString *)developperKey { return [NSString stringWithString:_developperKey]; } -// Get the development mode. -- (BOOL)developmentMode { - return [self guessDevmodeFromAPIKey]; -} - - (void)setLoggerDelegate:(id)loggerDelegate { _loggerDelegate = loggerDelegate; @@ -118,12 +100,12 @@ - (void)setAssociatedDomains:(NSArray *)domains { return _associatedDomains; } -#pragma mark - -#pragma mark Private methods +- (void)setDisabledMigrations:(BatchMigration)migrations { + _disabledMigrations = migrations; +} -// Try to guess the development mode from the key. -- (BOOL)guessDevmodeFromAPIKey { - return [_developperKey hasPrefix:@"DEV"]; +- (Boolean)isMigrationDisabledFor:(BatchMigration)migration { + return _disabledMigrations & migration; } @end diff --git a/Sources/Batch/Modules/Core/BACoreCenter.h b/Sources/Batch/Modules/Core/BACoreCenter.h index e5c6f38..064b87b 100644 --- a/Sources/Batch/Modules/Core/BACoreCenter.h +++ b/Sources/Batch/Modules/Core/BACoreCenter.h @@ -53,47 +53,12 @@ */ + (void)startWithAPIKey:(NSString *)key; -/*! - @method handleURL: - @abstract Give the URL to Batch systems. - @discussion Call this method in application:openURL:sourceApplication:annotation: of your UIApplicationDelegate - @discussion You can call this method from any thread. - @param url : The input URL. - @return YES if Batch take care of this URL, No otherwise. - @warning The delegate methods is always called in the main thread! - */ -+ (BOOL)handleURL:(NSURL *)url __attribute__((warn_unused_result)); - -/*! - @method isRunningInDevelopmentMode - @abstract Test if Batch is running in development mode. - @discussion You can call this method from any thread. - @return YES if Batch is Running AND if it uses a development API key. - */ -+ (BOOL)isRunningInDevelopmentMode __attribute__((warn_unused_result)); - /*! Open URL using UIApplication. Handles compatibility: prefer this method over calling UIApplication directly */ + (void)openURLWithUIApplication:(NSURL *)URL; -/*! - @method setUseAdvancedDeviceInformation: - @abstract Set if Batch can use advanced device identifiers (default = YES) - @discussion Advanced device identifiers include information about the device itself, but nothing that - directly identify the user, such as but not limited to: - - Device model - - Device brand - - Carrier name - - Setting this to false have a negative impact on core Batch features - You should only use it if you know what you are doing. - - @param use : YES if Batch can try to use advanced device information, NO if you don't want to - */ -+ (void)setUseAdvancedDeviceInformation:(BOOL)use; - /** Opens the given deeplink. Allows for developers to override the behavior using a deeplink delegate. diff --git a/Sources/Batch/Modules/Core/BACoreCenter.m b/Sources/Batch/Modules/Core/BACoreCenter.m index c01b2eb..6c91e74 100644 --- a/Sources/Batch/Modules/Core/BACoreCenter.m +++ b/Sources/Batch/Modules/Core/BACoreCenter.m @@ -42,6 +42,20 @@ #import #import +#import + +// BAVersion, BALevel and BAMessagingLevel have moved into Version.h and have been renamed +// Their values are not documented there because the Info.plist header parser is pretty dumb and will include comments +// If you need to get the user facing BAVersion, use BACoreCenter.sdkVersion + +// We need two intermedary function to make a quoted define from another define +// One expands the macro parameter +// The other adds the quotes +// It would be simpler to quote this in Versions.h but the plist preprocessor is dumb +#define BAStringifyValue(macro) #macro +#define BAGetStringifiedMacro(macro) BAStringifyValue(macro) +#define BASDKVersionNSString @BAGetStringifiedMacro(BASDKVersion) + #define LOGGER_DOMAIN @"Core" // Internal methods and parameters. @@ -50,9 +64,6 @@ @interface BACoreCenter () // Activate the whole Batch system. - (void)excecuteStartWithAPIKey:(NSString *)key; -// Test if Batch is running in development mode. -- (BOOL)executeIsDevelopmentMode; - // Resume any activity of BA. - (void)stop; @@ -86,47 +97,34 @@ + (void)startWithAPIKey:(NSString *)key { [[BACoreCenter instance] excecuteStartWithAPIKey:key]; } -// Give the URL to Batch systems. -+ (BOOL)handleURL:(NSURL *)url { - return NO; -} - -// Test if Batch is running in development mode. -+ (BOOL)isRunningInDevelopmentMode { - return [[BACoreCenter instance] executeIsDevelopmentMode]; -} - -+ (void)setUseAdvancedDeviceInformation:(BOOL)use { - [[BACoreCenter instance] setUseAdvancedDeviceInformation:use]; -} - - (void)openDeeplink:(NSString *)deeplink inApp:(BOOL)inApp { id developerDelegate = [self.configuration deeplinkDelegate]; - if (developerDelegate != nil) { - [BALogger debugForDomain:LOGGER_DOMAIN - message:@"Forwarding deeplink '%@' to developer implementation", deeplink]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (developerDelegate != nil) { + [BALogger debugForDomain:LOGGER_DOMAIN + message:@"Forwarding deeplink '%@' to developer implementation", deeplink]; - dispatch_async(dispatch_get_main_queue(), ^{ [developerDelegate openBatchDeeplink:deeplink]; - }); - } else { - NSURL *deeplinkURL = [NSURL URLWithString:deeplink]; - if (deeplinkURL != nil) { - if ([self openUniversalLinkIfPossible:deeplinkURL]) { - // We successfully opened universal link - return; - } - if (!inApp || ![self openDeeplinkURLInAppIfPossible:deeplinkURL]) { // don't open in app OR tried to open in - // app but failed - // then open with UIApplication - [BACoreCenter openURLWithUIApplication:deeplinkURL]; - } - } else { - [BALogger debugForDomain:LOGGER_DOMAIN - message:@"Tried to open deeplink '%@', but failed to convert it to a NSURL", deeplink]; - } - } + } else { + NSURL *deeplinkURL = [NSURL URLWithString:deeplink]; + if (deeplinkURL != nil) { + if ([self openUniversalLinkIfPossible:deeplinkURL]) { + // We successfully opened universal link + return; + } + if (!inApp || + ![self openDeeplinkURLInAppIfPossible:deeplinkURL]) { // don't open in app OR tried to open in + // app but failed + // then open with UIApplication + [BACoreCenter openURLWithUIApplication:deeplinkURL]; + } + } else { + [BALogger debugForDomain:LOGGER_DOMAIN + message:@"Tried to open deeplink '%@', but failed to convert it to a NSURL", deeplink]; + } + } + }); } #pragma mark - @@ -236,43 +234,40 @@ - (void)restart:(NSNotification *)notification { BOOL shouldWaitBeforeSendingStart = false; // Here be dragons, we're handling UIScene - if (@available(iOS 13.0, *)) { - if ([BAApplicationLifecycle applicationUsesUIScene]) { - // Check if it's a late call (like cordova, or just a bad integration), or if we're in - // application:didFinishLaunchingWithOptions:-ish - if (appState == - UIApplicationStateBackground && // Late starts (StateActive or StateInactive) should force a start - notification == nil // Only skip if we're not coming from a lifecycle notification - ) { - [BALogger debugForDomain:LOGGER_DOMAIN message:@"Waiting before tracking start"]; - shouldWaitBeforeSendingStart = true; - } + if ([BAApplicationLifecycle applicationUsesUIScene]) { + // Check if it's a late call (like cordova, or just a bad integration), or if we're in + // application:didFinishLaunchingWithOptions:-ish + if (appState == + UIApplicationStateBackground && // Late starts (StateActive or StateInactive) should force a start + notification == nil // Only skip if we're not coming from a lifecycle notification + ) { + [BALogger debugForDomain:LOGGER_DOMAIN message:@"Waiting before tracking start"]; + shouldWaitBeforeSendingStart = true; + } - // Update isPotentiallyInBackgroundRefresh using UIScene if we're in an ambiguous state - // This should not be ambiguous thanks to checking for UIApplicationWillEnterForegroundNotification - // but lets make sure we're not misreporting silent starts - if (isPotentiallyInBackgroundRefresh) { - isPotentiallyInBackgroundRefresh = ![BAApplicationLifecycle hasASceneInForegroundState]; - } + // Update isPotentiallyInBackgroundRefresh using UIScene if we're in an ambiguous state + // This should not be ambiguous thanks to checking for UIApplicationWillEnterForegroundNotification + // but lets make sure we're not misreporting silent starts + if (isPotentiallyInBackgroundRefresh) { + isPotentiallyInBackgroundRefresh = ![BAApplicationLifecycle hasASceneInForegroundState]; + } - // Now, we need to handle restarting - // Don't restart if we're getting UIApplicationWillEnterForegroundNotification on the first launch - // This only happens in UIScene apps. - // We can't rely on the UIScene state alone, as it's the same for first foreground and following ones on iOS - // 13 (iOS 14 fixes this, but who knows what 15 will break). Note: this is a quick hack. We should rework - // this, and start to the session manager. This solution looks like it might break at any moment, it already - // changed between iOS 13 and 14. - // - // We DO need to send the start, as we skipped it the first time. - if (isRestartingFromWillEnterForeground && appState == UIApplicationStateInactive) { - [BALogger - debugForDomain:LOGGER_DOMAIN - message: - @"Batch was asked to restart after UIApplicationWillEnterForegroundNotification but it " + // Now, we need to handle restarting + // Don't restart if we're getting UIApplicationWillEnterForegroundNotification on the first launch + // This only happens in UIScene apps. + // We can't rely on the UIScene state alone, as it's the same for first foreground and following ones on iOS + // 13 (iOS 14 fixes this, but who knows what 15 will break). Note: this is a quick hack. We should rework + // this, and start to the session manager. This solution looks like it might break at any moment, it already + // changed between iOS 13 and 14. + // + // We DO need to send the start, as we skipped it the first time. + if (isRestartingFromWillEnterForeground && appState == UIApplicationStateInactive) { + [BALogger + debugForDomain:LOGGER_DOMAIN + message:@"Batch was asked to restart after UIApplicationWillEnterForegroundNotification but it " @"looks like this is the app's first launch. Tracking start, but skipping other work."]; - [self callStartWebserviceWithSilentStart:false]; - return; - } + [self callStartWebserviceWithSilentStart:false]; + return; } } @@ -300,7 +295,7 @@ - (void)restart:(NSNotification *)notification { [self callStartWebserviceWithSilentStart:isPotentiallyInBackgroundRefresh]; } - if ([BACoreCenter isRunningInDevelopmentMode]) { + if ([self.configuration.developperKey hasPrefix:@"DEV"]) { [BALogger publicForDomain:nil message:@"Batch started with a DEV API key"]; } @@ -343,19 +338,6 @@ - (void)callStartWebserviceWithSilentStart:(BOOL)isSilentStart { }); } -// Test if Batch is running in development mode. -- (BOOL)executeIsDevelopmentMode { - if ([self.status isRunning] == NO) { - return NO; - } - - return [self.configuration developmentMode]; -} - -- (void)setUseAdvancedDeviceInformation:(BOOL)use { - [self.configuration setUseAdvancedDeviceInformation:use]; -} - // Resume any activity of BA. - (void)stop { // Change status. @@ -377,6 +359,11 @@ - (void)portDataFromVersion:(NSString *)version { @return YES if the URL was opened, NO if it couldn't. */ - (BOOL)openDeeplinkURLInAppIfPossible:(NSURL *)deeplinkURL { +#if TARGET_OS_VISION + // SFSafariViewController is not supported on visionOS: trying only adds bug. + // Safari is opened no matter what, skip the middleman. + return NO; +#endif @try { UIViewController *targetVC = [[BAWindowHelper keyWindow] rootViewController]; if (targetVC.presentedViewController != nil) { @@ -412,8 +399,7 @@ - (BOOL)openDeeplinkURLInAppIfPossible:(NSURL *)deeplinkURL { + (void)openURLWithUIApplication:(NSURL *)URL { [BALogger debugForDomain:LOGGER_DOMAIN message:@"Opening deeplink '%@' using UIApplication", URL.absoluteString]; - UIApplication *sharedApplication = [UIApplication sharedApplication]; - [sharedApplication openURL:URL options:@{} completionHandler:nil]; + [[UIApplication sharedApplication] openURL:URL options:@{} completionHandler:nil]; } /** @@ -451,27 +437,28 @@ - (BOOL)openUniversalLinkIfPossible:(NSURL *)URL { Boolean errorAlreadyLogged = false; - if (@available(iOS 13.0, *)) { - if ([BAApplicationLifecycle applicationUsesUIScene]) { - UIScene *scene = [[UIApplication sharedApplication].connectedScenes allObjects].firstObject; - id sceneDelegate = [scene delegate]; - if ([sceneDelegate respondsToSelector:@selector(scene:continueUserActivity:)]) { - [sceneDelegate scene:scene continueUserActivity:userActivity]; - return YES; - } else { - [BALogger debugForDomain:LOGGER_DOMAIN - message:@"It looks like scene:continueUserActivity: is not " - @"implemented, did you correctly add it to your SceneDelegate?"]; - errorAlreadyLogged = true; - } + if ([BAApplicationLifecycle applicationUsesUIScene]) { + UIScene *scene = [[UIApplication sharedApplication].connectedScenes allObjects].firstObject; + id sceneDelegate = [scene delegate]; + if ([sceneDelegate respondsToSelector:@selector(scene:continueUserActivity:)]) { + [sceneDelegate scene:scene continueUserActivity:userActivity]; + return YES; + } else { + [BALogger debugForDomain:LOGGER_DOMAIN + message:@"It looks like scene:continueUserActivity: is not " + @"implemented, did you correctly add it to your SceneDelegate?"]; + errorAlreadyLogged = true; } } id appDelegate = [UIApplication sharedApplication].delegate; - if ([appDelegate respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)]) { - [appDelegate application:[UIApplication sharedApplication] + UIApplication *currentApplication = [UIApplication sharedApplication]; + if (currentApplication && [appDelegate respondsToSelector:@selector(application: + continueUserActivity:restorationHandler:)]) { + [appDelegate application:currentApplication continueUserActivity:userActivity - restorationHandler:nil]; + restorationHandler:^(NSArray> *_Nullable restorableObjects){ + }]; return YES; } if (!errorAlreadyLogged) { @@ -497,15 +484,6 @@ - (void)checkForIncompatibilities { message:@"⚠️ More info about the manual integration here: " @"https://batch.com/doc/ios/advanced/manual-integration.html"]; } - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (![BABundleInfo isSharedGroupConfigured]) { - [BALogger publicForDomain:nil - message:@"⚠️ The App Group '%@' hasn't been configured. See the documentation for more " - @"info: https://doc.batch.com/ios/advanced/app-groups", - [BABundleInfo sharedGroupId]]; - } - }); } - (id)loggerDelegate { diff --git a/Sources/Batch/Modules/Core/BANotificationAuthorization.h b/Sources/Batch/Modules/Core/BANotificationAuthorization.h index 0ca67a7..a83daa8 100644 --- a/Sources/Batch/Modules/Core/BANotificationAuthorization.h +++ b/Sources/Batch/Modules/Core/BANotificationAuthorization.h @@ -1,7 +1,5 @@ #import -#import - typedef NS_OPTIONS(NSUInteger, BANotificationAuthorizationTypes) { BANotificationAuthorizationTypesNone = 0, BANotificationAuthorizationTypesBadge = 1 << 0, @@ -60,8 +58,6 @@ typedef NS_ENUM(NSUInteger, BANotificationAuthorizationStatus) { @property BANotificationAuthorizationTypes types; -@property BatchPushNotificationSettingStatus applicationSetting; - - (nullable NSDictionary *)optionalDictionaryRepresentation; - (nonnull NSDictionary *)dictionaryRepresentation; @@ -72,10 +68,6 @@ typedef NS_ENUM(NSUInteger, BANotificationAuthorizationStatus) { @property (nonnull) BANotificationAuthorizationSettings *currentSettings; -+ (BatchPushNotificationSettingStatus)applicationSettings; - -- (void)setApplicationSettings:(BatchPushNotificationSettingStatus)appSettings skipServerEvent:(BOOL)skipServerEvent; - - (BOOL)shouldFetchSettings; - (void)fetch:(void (^_Nullable)(BANotificationAuthorizationSettings *_Nonnull))completionHandler; diff --git a/Sources/Batch/Modules/Core/BANotificationAuthorization.m b/Sources/Batch/Modules/Core/BANotificationAuthorization.m index 8ad8bd2..b771140 100644 --- a/Sources/Batch/Modules/Core/BANotificationAuthorization.m +++ b/Sources/Batch/Modules/Core/BANotificationAuthorization.m @@ -7,14 +7,12 @@ #import #import #import -#import @implementation BANotificationAuthorizationSettings - (instancetype)init { self = [super init]; if (self) { - self.applicationSetting = -1; self.status = BANotificationAuthorizationStatusWaitingForValue; self.types = BANotificationAuthorizationTypesNone; } @@ -38,10 +36,6 @@ - (BOOL)isEqualToSettings:(BANotificationAuthorizationSettings *)object { return false; } - if (self.applicationSetting != object.applicationSetting) { - return false; - } - return true; } @@ -63,7 +57,6 @@ - (nonnull NSDictionary *)dictionaryRepresentation { return @{ @"status" : @(self.status), @"types" : @(self.types), - @"app_setting" : @(self.applicationSetting), }; } @@ -85,19 +78,6 @@ - (instancetype)init { return self; } -+ (BatchPushNotificationSettingStatus)applicationSettings { - return [(NSNumber *)[BAParameter objectForKey:kParametersAppNotificationSettingsKey - kindOfClass:[NSNumber class] - fallback:@(BatchPushNotificationSettingStatusUndefined)] unsignedIntegerValue]; -} - -- (void)setApplicationSettings:(BatchPushNotificationSettingStatus)appSettings skipServerEvent:(BOOL)skipServerEvent { - [BAParameter setValue:@(appSettings) forKey:kParametersAppNotificationSettingsKey saved:true]; - if (!skipServerEvent) { - [self settingsMayHaveChanged]; - } -} - - (BOOL)shouldFetchSettings { return self.currentSettings.status == BANotificationAuthorizationStatusUnknown; } @@ -158,7 +138,6 @@ - (void)fetchUN:(void (^_Nullable)(BANotificationAuthorizationSettings *_Nonnull } baSettings.types = types; - baSettings.applicationSetting = [BANotificationAuthorization applicationSettings]; [self updateSettings:baSettings completionHandler:completionHandler]; }]; @@ -231,7 +210,6 @@ - (nullable BANotificationAuthorizationSettings *)persistedSettings { if ([types isKindOfClass:[NSNumber class]] && [status isKindOfClass:[NSNumber class]]) { BANotificationAuthorizationSettings *settings = [BANotificationAuthorizationSettings new]; - settings.applicationSetting = [BANotificationAuthorization applicationSettings]; settings.types = [types unsignedIntegerValue]; settings.status = [status unsignedIntegerValue]; return settings; diff --git a/Sources/Batch/Modules/Core/BATInternalEvents.swift b/Sources/Batch/Modules/Core/BATInternalEvents.swift new file mode 100644 index 0000000..2730089 --- /dev/null +++ b/Sources/Batch/Modules/Core/BATInternalEvents.swift @@ -0,0 +1,13 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +public enum BATInternalEvent: String { + case profileIdentify = "_PROFILE_IDENTIFY" + case profileDataChanged = "_PROFILE_DATA_CHANGED" + case nativeDataChanged = "_NATIVE_DATA_CHANGED" +} diff --git a/Sources/Batch/Modules/Core/BAUserProfile.h b/Sources/Batch/Modules/Core/BAUserProfile.h index bcb30d9..89be220 100644 --- a/Sources/Batch/Modules/Core/BAUserProfile.h +++ b/Sources/Batch/Modules/Core/BAUserProfile.h @@ -43,13 +43,6 @@ */ @property (strong, nonatomic, nullable) NSString *region; -/*! - @property attributionID - @abstract The user attribution identifier. - @discussion Set to nil to reset the setting. - */ -@property (strong, nonatomic, nullable) NSString *attributionID; - /*! @method defaultUserProfile @abstract Access the default user profile object. @@ -65,12 +58,4 @@ */ - (NSDictionary *_Nonnull)dictionaryRepresentation __attribute__((warn_unused_result)); -/*! -@method incrementVersion -@abstract Increment the current version of user profile and send it to the server. -*/ -- (void)incrementVersion; - -- (void)sendAttributionIDChangedEvent; - @end diff --git a/Sources/Batch/Modules/Core/BAUserProfile.m b/Sources/Batch/Modules/Core/BAUserProfile.m index 52c7694..998ccab 100644 --- a/Sources/Batch/Modules/Core/BAUserProfile.m +++ b/Sources/Batch/Modules/Core/BAUserProfile.m @@ -53,7 +53,6 @@ - (NSDictionary *)dictionaryRepresentation { NSMutableDictionary *keyValues = [[NSMutableDictionary alloc] init]; [keyValues setValue:[self language] forKey:@"ula"]; [keyValues setValue:[self region] forKey:@"ure"]; - [keyValues setValue:[self version] forKey:@"upv"]; return [NSDictionary dictionaryWithDictionary:keyValues]; } @@ -72,38 +71,9 @@ - (NSError *)updateValue:(NSString *)value forKey:(NSString *)key { return error; } -- (void)incrementVersion { - @synchronized(_lock) { - NSNumber *version = [self version]; - // Sanity - if (![version isKindOfClass:[NSNumber class]]) { - [BAParameter setValue:@(1) forKey:kParametersAppProfileVersionKey saved:YES]; - } else { - [BAParameter setValue:@([version longLongValue] + 1) forKey:kParametersAppProfileVersionKey saved:YES]; - } - - NSMutableDictionary *eventParameters = [[self dictionaryRepresentation] mutableCopy]; - [eventParameters setValue:[self customIdentifier] forKey:@"cus"]; - - [BATrackerCenter trackPrivateEvent:@"_PROFILE_CHANGED" parameters:eventParameters]; - } -} - -- (void)sendAttributionIDChangedEvent { - NSMutableDictionary *params = [NSMutableDictionary dictionary]; - params[@"attribution_id"] = self.attributionID != nil ? self.attributionID : [NSNull null]; - [BATrackerCenter trackPrivateEvent:@"_ATTRIBUTION_ID_CHANGED" parameters:params]; -} - #pragma mark - #pragma mark Properties override methods -- (NSNumber *)version { - @synchronized(_lock) { - return [BAParameter objectForKey:kParametersAppProfileVersionKey fallback:@(1)]; - } -} - /*** Custom Identifier ***/ - (NSString *)customIdentifier { @@ -148,21 +118,6 @@ - (void)setRegion:(NSString *)region { } } -/*** Attribution Identifier ***/ - -- (NSString *)attributionID { - return [BAParameter objectForKey:kParametersAttributionIDKey fallback:nil]; -} - -- (void)setAttributionID:(NSString *)attributionID { - NSError *error = [self updateValue:attributionID forKey:kParametersAttributionIDKey]; - - if (error != nil) { - [BALogger errorForDomain:@"UserProfile" - message:@"Error changing the attribution identifier: %@", [error localizedDescription]]; - } -} - #pragma mark - #pragma mark NSCoding archive methods diff --git a/Sources/Batch/Modules/Data Collection/BATDataCollectionCenter.swift b/Sources/Batch/Modules/Data Collection/BATDataCollectionCenter.swift new file mode 100644 index 0000000..cc7bc2f --- /dev/null +++ b/Sources/Batch/Modules/Data Collection/BATDataCollectionCenter.swift @@ -0,0 +1,140 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +fileprivate let loggerDomain = "DataCollection" + +fileprivate let geoipUserDefaultKey = "geoip" +fileprivate let deviceModelUserDefaultKey = "deviceModel" + +/// Data Collection module. +@objcMembers +public class BATDataCollectionCenter: NSObject { + /// Singleton instance + public static let sharedInstance = BATDataCollectionCenter() + + /// The current data collection cofiguration + public let dataCollectionConfig = BatchDataCollectionConfig() + + // Constructor + override private init() { + super.init() + // Load data collection config from user defaults + loadDataCollectionConfig() + } + + /// Batch did started + public static func batchDidStart() { + // Check if some parameters have changed + BATDataCollectionCenter.sharedInstance.systemParametersMayHaveChanged() + } + + /// Check if some system parameters has changed + /// Method visible for testing + public func systemParametersMayHaveChanged() { + var data: [AnyHashable: Any] = [:] + for param in SystemParameterRegistry.all where param.watched { + guard let key = param.shortname.serializedName else { + continue + } + if param.hasChanged, param.allowed { + data[key] = param.lastValue ?? NSNull() + } + } + guard !data.isEmpty else { + BALogger.debug(domain: loggerDomain, message: "Native data has not changed.") + return + } + // Sending native data changed event + sendNativeDataChangedEvent(data) + } + + /// Build identifiers for query webservices + public func buildIdsForQuery() -> [AnyHashable: Any] { + var ids: [AnyHashable: Any] = [:] + + // Adding system parameters + for param in SystemParameterRegistry.all where param.allowed { + guard let value = param.value, !value.isEmpty else { + continue + } + ids[param.shortname.rawValue] = value + } + // Adding data collection + ids["data_collection"] = ["geoip": dataCollectionConfig.geoIPEnabled()?.boolValue ?? false] + + return ids + } + + /// Update the current data collection configuration + public func updateDataCollectionConfig(editor: BatchDataCollectionConfigEditor) { + let config = BatchDataCollectionConfig() + editor(config) + self.onDataCollectionConfigChanged(config) + } + + /// Handle modification of the data collection config + private func onDataCollectionConfigChanged(_ config: BatchDataCollectionConfig) { + // Ensure the config has changed + guard !BATDataCollectionUtils.areConfigsEquals(config, dataCollectionConfig) else { + BALogger.debug(domain: loggerDomain, message: "Automatic data collection config hasn't changed.") + return + } + + var data: [AnyHashable: Any] = [:] + + if let geoIPEnabled = config.geoIPEnabled(), geoIPEnabled != self.dataCollectionConfig.geoIPEnabled() { + data["geoip_resolution"] = geoIPEnabled.boolValue + self.dataCollectionConfig.setGeoIPEnabled(geoIPEnabled.boolValue) + } + + if let deviceModelEnabled = config.deviceModelEnabled(), deviceModelEnabled != self.dataCollectionConfig.deviceModelEnabled() { + SystemParameterRegistry.deviceModel.allowed = deviceModelEnabled.boolValue + if let key = SystemParameterRegistry.deviceModel.shortname.serializedName { + data[key] = deviceModelEnabled.boolValue ? SystemParameterRegistry.deviceModel.value : NSNull() + } + self.dataCollectionConfig.setDeviceModelEnabled(deviceModelEnabled.boolValue) + } + // Sending native data changed event + sendNativeDataChangedEvent(data) + + // Persist new config + self.persistDataCollectionConfig() + } + + /// Send a native data changed event with given data as paremeters + private func sendNativeDataChangedEvent(_ data: [AnyHashable: Any]) { + let eventTracker: BATEventTracker? = BAInjection.inject(BATEventTracker.self) + eventTracker?.trackPrivateEvent(event: .nativeDataChanged, parameters: data, collapsable: false) + } + + /// Persist the current data collection config as NSDictionnary into UserDefault + private func persistDataCollectionConfig() { + let dictionnaryConfig = [ + geoipUserDefaultKey: dataCollectionConfig.geoIPEnabled(), + deviceModelUserDefaultKey: dataCollectionConfig.deviceModelEnabled(), + ] + BAParameter.setValue(dictionnaryConfig, forKey: kParametersDataCollectionConfigKey, saved: true) + } + + /// Load data collection config from UserDefault + private func loadDataCollectionConfig() { + if let dictionnaryConfig = BAParameter.object(forKey: kParametersDataCollectionConfigKey, fallback: nil) as? [String: Any] { + self.dataCollectionConfig.setGeoIPEnabled((dictionnaryConfig[geoipUserDefaultKey] as? NSNumber)?.boolValue ?? false) + self.dataCollectionConfig.setDeviceModelEnabled((dictionnaryConfig[deviceModelUserDefaultKey] as? NSNumber)?.boolValue ?? false) + } else { + self.setDefaultDataCollectionConfig() + } + } + + /// Set default value for the current data collection configuration + /// Method visible for testing + public func setDefaultDataCollectionConfig() { + self.dataCollectionConfig.setGeoIPEnabled(false) + self.dataCollectionConfig.setDeviceModelEnabled(false) + } +} diff --git a/Sources/Batch/Modules/Data Collection/BATSystemParameter.swift b/Sources/Batch/Modules/Data Collection/BATSystemParameter.swift new file mode 100644 index 0000000..2041697 --- /dev/null +++ b/Sources/Batch/Modules/Data Collection/BATSystemParameter.swift @@ -0,0 +1,199 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +/// System Parameter shortname used in legacy webservices as query's ids +enum SystemParameterShortname: String { + case installationID = "di" + case customIdentifier = "cus" + case deviceRegion = "dre" + case deviceLanguage = "dla" + case deviceTimezone = "dtz" + case deviceDate = "da" + case sdkInstallDate = "did" + case deviceModel = "dty" + case deviceOSVersion = "osv" + case bundleID = "bid" + case applicationID = "pid" + case platform = "pl" + case apiLevel = "lvl" + case messagingAPILevel = "mlvl" + case appVersion = "apv" + case versionCode = "apc" + case pluginVersion = "plv" + case bridgeVersion = "brv" + case sessionIdentifier = "s" + + /// Event serialized name computed property + var serializedName: String? { + switch self { + case .installationID: + return nil + case .customIdentifier: + return nil + case .applicationID: + return nil + case .deviceDate: + return nil + case .platform: + return nil + case .sessionIdentifier: + return nil + case .deviceRegion: + return "device_region" + case .deviceLanguage: + return "device_language" + case .deviceTimezone: + return "device_timezone" + case .sdkInstallDate: + return "device_installation_date" + case .deviceModel: + return "device_model" + case .deviceOSVersion: + return "os_version" + case .bundleID: + return "app_bundle_id" + case .apiLevel: + return "sdk_api_level" + case .messagingAPILevel: + return "sdk_m_api_level" + case .appVersion: + return "app_version" + case .versionCode: + return "app_build_number" + case .pluginVersion: + return "plugin_version" + case .bridgeVersion: + return "bridge_version" + } + } +} + +/// System parameter registry +enum SystemParameterRegistry { + // MARK: Unwatched Parameters + + /// Installation identifier + public static let installationID = BATSystemParameter(shortname: .installationID, watched: false, allowed: true) + + /// Custom user identier + public static let customUserID = BATSystemParameter(shortname: .customIdentifier, watched: false, allowed: true) + + /// User's session identier + public static let sessionID = BATSystemParameter(shortname: .sessionIdentifier, watched: false, allowed: true) + + /// Current device date + public static let deviceDate = BATSystemParameter(shortname: .deviceDate, watched: false, allowed: true) + + /// Application id + public static let applicationID = BATSystemParameter(shortname: .applicationID, watched: false, allowed: true) + + /// Platform + public static let platform = BATSystemParameter(shortname: .platform, watched: false, allowed: true) + + // MARK: Watched Parameters + + /// Device region + public static let deviceRegion = BATSystemParameter(shortname: .deviceRegion, watched: true, allowed: true) + + /// Device Language + public static let deviceLanguage = BATSystemParameter(shortname: .deviceLanguage, watched: true, allowed: true) + + /// Device timezone + public static let deviceTimezone = BATSystemParameter(shortname: .deviceTimezone, watched: true, allowed: true) + + /// SDK Install date + public static let sdkInstallDate = BATSystemParameter(shortname: .sdkInstallDate, watched: true, allowed: true) + + /// Device model + public static let deviceModel = BATSystemParameter(shortname: .deviceModel, watched: true, allowed: BATDataCollectionCenter.sharedInstance.dataCollectionConfig.deviceModelEnabled()?.boolValue ?? false) + + /// Device OS version + public static let deviceOSVersion = BATSystemParameter(shortname: .deviceOSVersion, watched: true, allowed: true) + + /// Bundle id + public static let bundleID = BATSystemParameter(shortname: .bundleID, watched: true, allowed: true) + + /// API level + public static let apiLevel = BATSystemParameter(shortname: .apiLevel, watched: true, allowed: true) + + /// Messaging API level + public static let messagingAPILevel = BATSystemParameter(shortname: .messagingAPILevel, watched: true, allowed: true) + + // App version + public static let appVersion = BATSystemParameter(shortname: .appVersion, watched: true, allowed: true) + + // App version code + public static let versionCode = BATSystemParameter(shortname: .versionCode, watched: true, allowed: true) + + // Plugin version + public static let pluginVersion = BATSystemParameter(shortname: .pluginVersion, watched: true, allowed: true) + + // Bridge version + public static let bridgeVersion = BATSystemParameter(shortname: .bridgeVersion, watched: true, allowed: true) + + /// All registred system parameters + public static let all = [installationID, customUserID, sessionID, deviceDate, deviceRegion, deviceLanguage, deviceTimezone, sdkInstallDate, deviceModel, deviceOSVersion, bundleID, applicationID, platform, apiLevel, messagingAPILevel, appVersion, versionCode, pluginVersion, bridgeVersion] +} + +/// System Parameter +class BATSystemParameter { + /// Shortname of the parameter + let shortname: SystemParameterShortname + + /// Whether this parameter is watched + let watched: Bool + + /// Whether this pameter is allowed to be sent + var allowed: Bool + + /// Init method + init(shortname: SystemParameterShortname, watched: Bool, allowed: Bool) { + self.shortname = shortname + self.watched = watched + self.allowed = allowed + } + + /// Get the current value + var value: String? { + return BAPropertiesCenter.value(forShortName: self.shortname.rawValue) + } + + /// Get last value read from NSUserDefault + var lastValue: String? { + return BAParameter.object(forKey: self.userDefaultKey, fallback: nil) as! String? + } + + /// Compute key for user default storage + var userDefaultKey: String { + return kParametersSystemParameterPrefix + self.shortname.rawValue + } + + /// Detect if the values has changed since the last time we get it. + /// This will save the new value if it changed + var hasChanged: Bool { + // Ensure the parameter is watched + guard watched else { + return false + } + // Getting current value of the parameter + let currentValue = self.value + + // Check if value has changed since the last time + let hasChanged = self.lastValue != currentValue + + if hasChanged { + // Saving new value (remove if null) + if currentValue == nil { + BAParameter.removeObject(forKey: self.userDefaultKey) + } else { + BAParameter.setValue(currentValue!, forKey: self.userDefaultKey, saved: true) + } + } + return hasChanged + } +} diff --git a/Sources/Batch/Modules/Data Collection/DataCollectionUtils.swift b/Sources/Batch/Modules/Data Collection/DataCollectionUtils.swift new file mode 100644 index 0000000..a5111b8 --- /dev/null +++ b/Sources/Batch/Modules/Data Collection/DataCollectionUtils.swift @@ -0,0 +1,31 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +struct BATDataCollectionUtils { + /// Method to compare two batch data collection config. + /// + /// Globally used to check if a new config is different from the old one + /// So if one of the configs field is null, we consider it as equal because we consider a null field as an unchanged value from the previous configuration. + /// - Parameters: + /// - config: The new config + /// - config2: The old config + /// - Returns: True if config are unchanged. Example: null, true, false, et false, true, false will return true.. + public static func areConfigsEquals(_ config: BatchDataCollectionConfig, _ config2: BatchDataCollectionConfig) -> Bool { + var sameGeoip = true + var sameDeviceModel = true + + if let config1GeoIpEnabled = config.geoIPEnabled(), let config2GeoIpEnabled = config2.geoIPEnabled() { + sameGeoip = config1GeoIpEnabled == config2GeoIpEnabled + } + + if let config1DeviceModelEnabled = config.deviceModelEnabled(), let config2DeviceModelEnabled = config2.deviceModelEnabled() { + sameDeviceModel = config1DeviceModelEnabled == config2DeviceModelEnabled + } + return sameGeoip && sameDeviceModel + } +} diff --git a/Sources/Batch/Modules/Inbox/BAInbox.m b/Sources/Batch/Modules/Inbox/BAInbox.m index 8208e39..f5b0c11 100644 --- a/Sources/Batch/Modules/Inbox/BAInbox.m +++ b/Sources/Batch/Modules/Inbox/BAInbox.m @@ -315,7 +315,6 @@ - (void)markNotificationAsDeleted:(nonnull BatchInboxNotificationContent *)notif [BATrackerCenter trackPrivateEvent:@"_INBOX_MARK_DELETED" parameters:eventData]; } internalNotification.isDeleted = true; - [notification _markAsDeleted]; [[BAInjection injectProtocol:@protocol(BAInboxDatasourceProtocol)] markAsDeleted:internalNotification.identifiers.identifier]; [_fetchedMessages removeObject:internalNotification]; @@ -474,6 +473,10 @@ - (void)syncFromWSForCursor:(NSString *)cursor NSMutableArray *models = [NSMutableArray new]; for (BAInboxNotificationContent *privateModel in privateModels) { + if (privateModel.isDeleted) { + continue; + } + BatchInboxNotificationContent *model = [[BatchInboxNotificationContent alloc] initWithInternalIdentifier:privateModel.identifiers.identifier rawPayload:privateModel.payload diff --git a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.h b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.h index 70131d2..65bbbf1 100644 --- a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.h +++ b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.h @@ -13,7 +13,7 @@ #import #import -@class BatchEventData; +@class BatchEventAttributes; /* Batch's In-App Messaging Module. @@ -55,7 +55,7 @@ */ - (void)processTrackerPublicEventNamed:(nonnull NSString *)name label:(nullable NSString *)label - data:(nullable BatchEventData *)data; + attributes:(nullable BatchEventAttributes *)attributes; /** Notify this module of the display of an In-App Campaign. diff --git a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.m b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.m index 679a6b4..28208ea 100644 --- a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.m +++ b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsCenter.m @@ -19,7 +19,7 @@ #import #import #import -#import +#import #import #import @@ -104,12 +104,18 @@ - (void)setup { _persistenceQueue = dispatch_queue_create_with_target("com.batch.localcampaigns.persistence", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)); +#if TARGET_OS_VISION + [BALogger debugForDomain:LOGGER_DOMAIN + message:@"Not registering Local Campaigns refresh: unsupported on visionOS."]; + return; +#else // New session is used to load the campaign cache, scheduling server refreshs // and emitting BANewSessionSignal [[BANotificationCenter defaultCenter] addObserver:self selector:@selector(newSessionStartedNotification) name:BATNewSessionStartedNotification object:nil]; +#endif } - (BALocalCampaignsManager *)campaignManager { @@ -125,6 +131,10 @@ - (BALocalCampaignsTracker *)viewTracker { } - (void)emitSignal:(id)signal { +#if TARGET_OS_VISION + [BALogger debugForDomain:LOGGER_DOMAIN message:@"Not handling Local Campaigns signal: unsupported on visionOS."]; + return; +#else if ([[BAOptOut instance] isOptedOut]) { [BALogger debugForDomain:LOGGER_DOMAIN message:@"Batch is opted-out from, not bubbling local campaigns signal"]; return; @@ -167,6 +177,7 @@ - (void)emitSignal:(id)signal { [self electCampaignForSignal:signal]; } }); +#endif } /** @@ -273,8 +284,8 @@ - (void)processTrackerPrivateEventNamed:(nonnull NSString *)name { - (void)processTrackerPublicEventNamed:(nonnull NSString *)name label:(nullable NSString *)label - data:(nullable BatchEventData *)data { - [self emitSignal:[[BAPublicEventTrackedSignal alloc] initWithName:name label:label data:data]]; + attributes:(nullable BatchEventAttributes *)attributes { + [self emitSignal:[[BAPublicEventTrackedSignal alloc] initWithName:name label:label attributes:attributes]]; } - (void)didPerformCampaignOutputWithIdentifier:(nonnull NSString *)identifier eventData:(nullable NSObject *)eventData { diff --git a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsManager.m b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsManager.m index 8c5be6d..0c08f08 100644 --- a/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsManager.m +++ b/Sources/Batch/Modules/Local Campaigns/BALocalCampaignsManager.m @@ -21,6 +21,7 @@ #import #import #import +#import #define LOG_DOMAIN @"LocalCampaignsManager" diff --git a/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.h b/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.h index e3afe3a..3e7d017 100644 --- a/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.h +++ b/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.h @@ -9,7 +9,7 @@ #import -@class BatchEventData; +@class BatchEventAttributes; @interface BAPublicEventTrackedSignal : NSObject @@ -17,10 +17,10 @@ @property (nullable, copy) NSString *label; -@property (nullable, assign) BatchEventData *data; +@property (nullable, assign) BatchEventAttributes *attributes; - (nonnull instancetype)initWithName:(nonnull NSString *)name label:(nullable NSString *)label - data:(nullable BatchEventData *)data; + attributes:(nullable BatchEventAttributes *)attributes; @end diff --git a/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.m b/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.m index e00290f..93a679c 100644 --- a/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.m +++ b/Sources/Batch/Modules/Local Campaigns/Signals/BAPublicEventTrackedSignal.m @@ -10,12 +10,14 @@ @implementation BAPublicEventTrackedSignal -- (instancetype)initWithName:(NSString *)name label:(nullable NSString *)label data:(nullable BatchEventData *)data { +- (instancetype)initWithName:(NSString *)name + label:(nullable NSString *)label + attributes:(nullable BatchEventAttributes *)attributes { self = [super init]; if (self) { self.name = name; self.label = label; - self.data = data; + self.attributes = attributes; } return self; diff --git a/Sources/Batch/Modules/Messaging/BAMSGImageDownloader.m b/Sources/Batch/Modules/Messaging/BAMSGImageDownloader.m index c3ecce9..74ab859 100644 --- a/Sources/Batch/Modules/Messaging/BAMSGImageDownloader.m +++ b/Sources/Batch/Modules/Messaging/BAMSGImageDownloader.m @@ -66,11 +66,7 @@ + (void)downloadImageForURL:(NSURL *_Nonnull)url NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.timeoutIntervalForResource = timeout; // Enforce TLS 1.2 - if (@available(iOS 13, tvOS 13, watchOS 6, macOS 10.15, *)) { - config.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; - } else { - config.TLSMinimumSupportedProtocol = kTLSProtocol12; - } + config.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil]; diff --git a/Sources/Batch/Modules/Messaging/BAMSGPayloadParser.m b/Sources/Batch/Modules/Messaging/BAMSGPayloadParser.m index 48263ad..b1a0779 100644 --- a/Sources/Batch/Modules/Messaging/BAMSGPayloadParser.m +++ b/Sources/Batch/Modules/Messaging/BAMSGPayloadParser.m @@ -9,6 +9,7 @@ #import #import #import +#import #define LOGGER_DOMAIN @"BAMSGPayloadParser" diff --git a/Sources/Batch/Modules/Messaging/BAMessagingCenter.h b/Sources/Batch/Modules/Messaging/BAMessagingCenter.h index a5eb2ad..0a0db76 100644 --- a/Sources/Batch/Modules/Messaging/BAMessagingCenter.h +++ b/Sources/Batch/Modules/Messaging/BAMessagingCenter.h @@ -24,7 +24,7 @@ extern NSString *_Nonnull const kBATMessagingMessageDidDisappear; NS_ASSUME_NONNULL_BEGIN -@interface BAMessagingCenter : NSObject +@interface BAMessagingCenter : NSObject @property (readonly) BOOL automaticMode; @@ -42,6 +42,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)setDelegate:(id _Nullable)delegate; +- (id _Nullable)delegate; + - (void)setImageDownloadTimeout:(NSTimeInterval)timeout; - (void)setCanReconfigureAVAudioSession:(BOOL)canReconfigureAVAudioSession; diff --git a/Sources/Batch/Modules/Messaging/BAMessagingCenter.m b/Sources/Batch/Modules/Messaging/BAMessagingCenter.m index 902a6e6..baf1abb 100644 --- a/Sources/Batch/Modules/Messaging/BAMessagingCenter.m +++ b/Sources/Batch/Modules/Messaging/BAMessagingCenter.m @@ -156,6 +156,10 @@ - (void)setDelegate:(id _Nullable)delegate { _wrappedDelegate = [[BABatchMessagingDelegateWrapper alloc] initWithDelgate:delegate]; } +- (id _Nullable)delegate { + return _wrappedDelegate.delegate; +} + - (void)setImageDownloadTimeout:(NSTimeInterval)timeout { _imageDownloadTimeout = timeout; } @@ -258,6 +262,11 @@ - (UIViewController *_Nullable)loadViewControllerForMessage:(BatchMessage *_Nonn } - (BOOL)presentMessagingViewController:(nonnull UIViewController *)vc error:(NSError **)error { +#if TARGET_OS_VISION + [BALogger publicForDomain:@"Messaging" + message:@"Refusing to presentMessagingViewController: unsupported on visionOS"]; + return false; +#else if (![NSThread currentThread].isMainThread) { [BALogger publicForDomain:@"Messaging" message:@"[BatchMessaging presentMessagingViewController:] was called outside of the main " @@ -325,10 +334,16 @@ - (BOOL)presentMessagingViewController:(nonnull UIViewController *)vc error:(NSE return false; } } +#endif } - (UIViewController *_Nullable)internalLoadViewControllerForMessage:(BatchMessage *_Nonnull)message error:(NSError *_Nullable *_Nullable)error { +#if TARGET_OS_VISION + [BALogger publicForDomain:@"Messaging" + message:@"Refusing to load messaging view controller: unsupported on visionOS"]; + return nil; +#else if (error) { *error = nil; } @@ -460,6 +475,7 @@ - (UIViewController *_Nullable)internalLoadViewControllerForMessage:(BatchMessag return nil; } +#endif } - (BOOL)performAction:(nonnull BAMSGAction *)action source:(nullable id)source { @@ -744,6 +760,10 @@ - (BAEventDispatcherCenter *)eventDispatcher { - (BOOL)displayMessage:(BatchMessage *_Nonnull)message bypassDnD:(BOOL)bypassDnD error:(NSError *_Nullable *_Nullable)outErr { +#if TARGET_OS_VISION + [BALogger publicForDomain:LOGGER_DOMAIN message:@"Batch Messaging is unsupported on visionOS."]; + return false; +#else if (self.doNotDisturb && !bypassDnD) { [BALogger publicForDomain:LOGGER_DOMAIN @@ -776,6 +796,7 @@ - (BOOL)displayMessage:(BatchMessage *_Nonnull)message } return false; } +#endif } - (BAMSGMessage *)messageForObject:(id)object { @@ -998,15 +1019,20 @@ - (BOOL)showViewControllerInOwnWindow:(UIViewController *)vc error:(NSError **)e } BAMSGOverlayWindow *window; - if (@available(iOS 13.0, *)) { - UIWindowScene *scene = [BAWindowHelper keyWindow].windowScene; - if (scene != nil) { - window = [[BAMSGOverlayWindow alloc] initWithWindowScene:scene]; - } + UIWindowScene *scene = [BAWindowHelper keyWindow].windowScene; + if (scene != nil) { + window = [[BAMSGOverlayWindow alloc] initWithWindowScene:scene]; } if (window == nil) { +#if TARGET_OS_VISION + // Vision pro has no way to get the screen size as it does not _really_ have + // a screen size in VR space. We have to use a default one + // FIXME: Fix this to reenable In-Apps + window = [[BAMSGOverlayWindow alloc] initWithFrame:CGRectMake(0, 0, 1280, 720)]; +#else window = [[BAMSGOverlayWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; +#endif } if ([vc conformsToProtocol:@protocol(BAMSGWindowHolder)]) { // This NEEDS to be before rootViewController is set on the window, as this will trigger viewDidLoad @@ -1054,37 +1080,4 @@ - (void)presentLandingMessage:(BatchMessage *_Nonnull)message bypassDnD:(BOOL)by }]; } -#pragma mark UIAlertView delegate methods - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)didPresentAlertView:(UIAlertView *)alertView { - [self messageShown:[self messageForObject:alertView]]; -} - -- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - BAMSGMessageAlert *message = (BAMSGMessageAlert *)[self messageForObject:alertView]; - - if (![message isKindOfClass:[BAMSGMessageAlert class]]) { - message = nil; - } - - [self messageDismissed:message]; - - // We can do this since our alert views only have one other CTA - if (buttonIndex != [alertView cancelButtonIndex]) { - // We don't need to handle BAMSGCTAActionKindClose since the UIAlertView always dismisses itself - [self messageButtonClicked:message ctaIndex:0 action:message.acceptCTA]; - [self performAction:message.acceptCTA - source:message.sourceMessage - actionIndex:0 - messageIdentifier:message.sourceMessage.devTrackingIdentifier]; - } else { - [self messageClosed:message]; - } -} - -#pragma clang diagnostic pop - @end diff --git a/Sources/Batch/Modules/Messaging/FeedbackGenerators.swift b/Sources/Batch/Modules/Messaging/FeedbackGenerators.swift new file mode 100644 index 0000000..19e9895 --- /dev/null +++ b/Sources/Batch/Modules/Messaging/FeedbackGenerators.swift @@ -0,0 +1,87 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +// Wrapper around UIImpactFeedbackGenerator that is safe to use on visionOS +@objc +@objcMembers +public class BATImpactFeedbackGenerator: NSObject { + @objc(BATImpactFeedbackStyle) + public enum FeedbackStyle: UInt { + case light + case medium + case heavy + } + + #if !os(visionOS) + let feedbackGenerator: UIImpactFeedbackGenerator + #endif + + public required init(style: FeedbackStyle) { + #if !os(visionOS) + let convertedStyle: UIImpactFeedbackGenerator.FeedbackStyle = switch style { + case .light: UIImpactFeedbackGenerator.FeedbackStyle.light + case .medium: UIImpactFeedbackGenerator.FeedbackStyle.medium + case .heavy: UIImpactFeedbackGenerator.FeedbackStyle.heavy + } + self.feedbackGenerator = UIImpactFeedbackGenerator(style: convertedStyle) + #endif + super.init() + } + + public func prepare() { + #if !os(visionOS) + self.feedbackGenerator.prepare() + #endif + } + + public func impactOccurred() { + #if !os(visionOS) + self.feedbackGenerator.impactOccurred() + #endif + } +} + +// Wrapper around UINotificationFeedbackGenerator that is safe to use on visionOS +@objc +@objcMembers +public class BATNotificationFeedbackGenerator: NSObject { + @objc(BATNotificationFeedbackType) + public enum FeedbackType: UInt { + case success + case warning + case error + } + + #if !os(visionOS) + let feedbackGenerator: UINotificationFeedbackGenerator + #endif + + override public required init() { + #if !os(visionOS) + self.feedbackGenerator = UINotificationFeedbackGenerator() + #endif + super.init() + } + + public func prepare() { + #if !os(visionOS) + self.feedbackGenerator.prepare() + #endif + } + + public func notificationOccurred(_ type: FeedbackType) { + #if !os(visionOS) + let convertedType: UINotificationFeedbackGenerator.FeedbackType = switch type { + case .success: UINotificationFeedbackGenerator.FeedbackType.success + case .warning: UINotificationFeedbackGenerator.FeedbackType.warning + case .error: UINotificationFeedbackGenerator.FeedbackType.error + } + feedbackGenerator.notificationOccurred(convertedType) + #endif + } +} diff --git a/Sources/Batch/Modules/Messaging/UI/BAMSGBaseBannerViewController.m b/Sources/Batch/Modules/Messaging/UI/BAMSGBaseBannerViewController.m index da5fa50..ff766dd 100644 --- a/Sources/Batch/Modules/Messaging/UI/BAMSGBaseBannerViewController.m +++ b/Sources/Batch/Modules/Messaging/UI/BAMSGBaseBannerViewController.m @@ -170,6 +170,14 @@ - (UIViewController *)guessOverlayedViewController { return guessedVC; } +#if TARGET_OS_VISION +- (UIContainerBackgroundStyle)preferredContainerBackgroundStyle { + // On visionOS, we do not want banners to appear on an opaque background blurring out the app beneath it. + // In an ideal world, we'd display the banners in dedicated windows, but for now this will have to do + return UIContainerBackgroundStyleHidden; +} +#endif + - (BOOL)prefersStatusBarHidden { return false; } @@ -871,17 +879,14 @@ - (void)applyContainerViewRulesForNode:(BACSSDOMNode *)node } } - id topGuide = self.topLayoutGuide; - // Padding goes before everything - [constraints - addObjectsFromArray: - [NSLayoutConstraint - constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|-(==%f@800)-[innerView]-(==%f@800)-|", - padding.top, padding.bottom] - options:0 - metrics:nil - views:NSDictionaryOfVariableBindings(topGuide, innerView)]]; + [constraints addObjectsFromArray:[NSLayoutConstraint + constraintsWithVisualFormat: + [NSString stringWithFormat:@"V:|-(==%f@800)-[innerView]-(==%f@800)-|", + padding.top, padding.bottom] + options:0 + metrics:nil + views:NSDictionaryOfVariableBindings(innerView)]]; [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat: diff --git a/Sources/Batch/Modules/Messaging/UI/BAMSGImageViewController.m b/Sources/Batch/Modules/Messaging/UI/BAMSGImageViewController.m index f020718..11facf1 100644 --- a/Sources/Batch/Modules/Messaging/UI/BAMSGImageViewController.m +++ b/Sources/Batch/Modules/Messaging/UI/BAMSGImageViewController.m @@ -15,6 +15,7 @@ #import #import #import +#import #import @interface BAMSGTapControl : UIControl @@ -36,7 +37,7 @@ @interface BAMSGImageViewController () *items = @[ [UIAction actionWithTitle:BAMSGWebviewDevMenuReload image:[UIImage systemImageNamed:@"arrow.clockwise"] diff --git a/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.h b/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.h index 5ad7852..27d1150 100644 --- a/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.h +++ b/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.h @@ -66,9 +66,6 @@ typedef NS_ENUM(NSUInteger, BATWebviewJavascriptBridgeErrorCode) { - (BAPromise *)installationID; -// Method that facilitates testing the attribution ID handling -- (nullable NSString *)readAttributionIDFromSDK; - - (BAPromise *)customRegion; - (BAPromise *)customLanguage; diff --git a/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.m b/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.m index 3963a8f..1575a66 100644 --- a/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.m +++ b/Sources/Batch/Modules/Messaging/Webview/BATWebviewJavascriptBridge.m @@ -155,21 +155,8 @@ This method does all of the heavy lifting (argument checking, error handling): j return [BAPromise resolved:[BAInstallationID installationID]]; } -// Method that facilitates testing the attribution ID handling -- (nullable NSString *)readAttributionIDFromSDK { - return [BAPropertiesCenter valueForShortName:@"idfa"]; -} - - (BAPromise *)attributionID { - BAPromise *promise = [BAPromise new]; - // TODO: Wall this behind a compilation flag - NSString *identifier = [self readAttributionIDFromSDK]; - if ([BANullHelper isStringEmpty:identifier]) { - [promise reject:[self makePublicError:@"No attribution identifier found."]]; - } else { - [promise resolve:identifier]; - } - return promise; + return [BAPromise rejected:[self makePublicError:@"Attribution identifier is not supported anymore"]]; } - (BAPromise *)customRegion { diff --git a/Sources/Batch/Modules/Messaging/Widgets/BAMSGActivityIndicatorView.m b/Sources/Batch/Modules/Messaging/Widgets/BAMSGActivityIndicatorView.m index 32a39ed..8cc51ef 100644 --- a/Sources/Batch/Modules/Messaging/Widgets/BAMSGActivityIndicatorView.m +++ b/Sources/Batch/Modules/Messaging/Widgets/BAMSGActivityIndicatorView.m @@ -101,26 +101,14 @@ - (void)setPreferredColor:(BAMSGActivityIndicatorViewColor)preferredColor { + (UIActivityIndicatorViewStyle)styleForPreferredSize:(BAMSGActivityIndicatorViewSize)preferredSize { UIActivityIndicatorViewStyle style; - if (@available(iOS 13.0, *)) { - switch (preferredSize) { - case BAMSGActivityIndicatorViewSizeLarge: - style = UIActivityIndicatorViewStyleLarge; - break; - case BAMSGActivityIndicatorViewSizeMedium: - default: - style = UIActivityIndicatorViewStyleMedium; - break; - } - } else { - switch (preferredSize) { - case BAMSGActivityIndicatorViewSizeLarge: - style = UIActivityIndicatorViewStyleWhiteLarge; - break; - case BAMSGActivityIndicatorViewSizeMedium: - default: - style = UIActivityIndicatorViewStyleWhite; - break; - } + switch (preferredSize) { + case BAMSGActivityIndicatorViewSizeLarge: + style = UIActivityIndicatorViewStyleLarge; + break; + case BAMSGActivityIndicatorViewSizeMedium: + default: + style = UIActivityIndicatorViewStyleMedium; + break; } return style; } diff --git a/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAlertContainerView.m b/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAlertContainerView.m index cb76ede..5b073c7 100644 --- a/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAlertContainerView.m +++ b/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAlertContainerView.m @@ -6,6 +6,7 @@ // #import +#import // Various constants that define the alert pan look & feel #define ANIMATION_DURATION 0.5 @@ -24,7 +25,7 @@ @implementation BAMSGPannableAlertContainerView { // Gesture recognizer state dependent variable BOOL _shouldDismiss; - UIImpactFeedbackGenerator *_hapticFeedbackGenerator; + BATImpactFeedbackGenerator *_hapticFeedbackGenerator; CGPoint _linkedViewInitialOffset; CGFloat _initialAlpha; CGFloat _linkedViewInitialAlpha; @@ -73,7 +74,7 @@ - (void)viewDragged:(UIPanGestureRecognizer *)recognizer { - (void)configureHapticFeedbackForState:(UIGestureRecognizerState)state { switch (state) { case UIGestureRecognizerStateBegan: - _hapticFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + _hapticFeedbackGenerator = [[BATImpactFeedbackGenerator alloc] initWithStyle:BATImpactFeedbackStyleMedium]; [_hapticFeedbackGenerator prepare]; break; case UIGestureRecognizerStateCancelled: diff --git a/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAnchoredContainerView.m b/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAnchoredContainerView.m index 0cbe537..e5b8861 100644 --- a/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAnchoredContainerView.m +++ b/Sources/Batch/Modules/Messaging/Widgets/BAMSGPannableAnchoredContainerView.m @@ -1,4 +1,5 @@ #import +#import #define DISMISS_THRESHOLD_TRANSLATE_HEIGHT_RATIO 0.5 #define DISMISS_THRESHOLD_MINIMUM_VELOCITY 100 @@ -67,9 +68,15 @@ - (void)didDetectPan:(UIPanGestureRecognizer *)recognizer { heightToHide = viewFrame.origin.y + viewFrame.size.height; NSLog(@"origin y %f, height %f", viewFrame.origin.y, viewFrame.size.height); } else { - // Here we'll have to take the distance between the screen's height and the view's Y position - // No need to add the view height, as this calculation already takes it into account + // Here we'll have to take the distance between the screen's height and the view's Y position + // No need to add the view height, as this calculation already takes it into account +#if TARGET_OS_VISION + // visionOS has no mainScreen, but I don't feel like risking this for legacy code + heightToHide = + [BAWindowHelper keyWindow].coordinateSpace.bounds.size.height - viewFrame.origin.y; +#else heightToHide = [[UIScreen mainScreen] bounds].size.height - viewFrame.origin.y; +#endif } // Shadows are outside of the layout computation diff --git a/Sources/Batch/Modules/Messaging/Widgets/BAMSGRemoteImageView.m b/Sources/Batch/Modules/Messaging/Widgets/BAMSGRemoteImageView.m index 25604e4..2e93aa0 100644 --- a/Sources/Batch/Modules/Messaging/Widgets/BAMSGRemoteImageView.m +++ b/Sources/Batch/Modules/Messaging/Widgets/BAMSGRemoteImageView.m @@ -18,7 +18,8 @@ - (void)setup { [super setup]; _activityIndicator = - [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; + [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; + _activityIndicator.color = [UIColor whiteColor]; [_activityIndicator hidesWhenStopped]; [self addSubview:_activityIndicator]; diff --git a/Sources/Batch/Modules/Profile/BAProfileCenter.swift b/Sources/Batch/Modules/Profile/BAProfileCenter.swift new file mode 100644 index 0000000..18b4efb --- /dev/null +++ b/Sources/Batch/Modules/Profile/BAProfileCenter.swift @@ -0,0 +1,219 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +fileprivate let loggerDomain = "Profile" + +@objc +public protocol BAProfileCenterProtocol { + func identify(_ customID: String?) + + /// Track a public event. Do not add E. to the event name. + /// This method does the validation for you and is meant to be called directly from the public API. + /// - Throws: BATValidationError + func trackPublicEvent(name: String, attributes: BatchEventAttributes?) throws + + /// Track a location + func trackLocation(_ location: CLLocation) + + /// Validate event attributes + /// Returns an array of human readable errors. If empty, the event validated successfully. + func validateEventAttributes(_ attributes: BatchEventAttributes) -> [String] + + /// Send a profile edition operation + func applyEditor(_ profileEditor: BATProfileEditor) + + /// Callback when a project has changed + @objc(onProjectChanged:withNewKey:) + func onProjectChanged(oldProjectKey: String?, newProjectKey: String?) +} + +// Profile module. +// Note: This should conform to BACenterProtocol but we can't do that in swift +// as it results in a cyclic dependency. But who cares this isn't mandatory anyway +// as we can still get callbacks thanks to Obj-C being weakly typed. +@objc +@objcMembers +public class BAProfileCenter: NSObject, BAProfileCenterProtocol { + public static let sharedInstance = BAProfileCenter() + + private static let eventNameValidationRegexpPattern = "^[a-zA-Z0-9_]{1,30}$" + + private let eventNameValidationRegexp = BATRegularExpression(pattern: BAProfileCenter.eventNameValidationRegexpPattern) + + override public init() { + super.init() + if eventNameValidationRegexp.regexpFailedToInitialize { + BALogger.public(domain: loggerDomain, message: "Error while creating event name regexp. Event tracking will not be possible.") + } + } + + public func identify(_ customID: String?) { + if let customID { + guard BATProfileDataValidators.isCustomIDTooLong(customID) == false else { + BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is invalid: it must not be longer than \(BATProfileDataValidators.customIDMaxLength) characters.") + return + } + + guard BATProfileDataValidators.isCustomIDAllowed(customID) else { + BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is invalid: it cannot be only made of whitespace or contain a newline.") + return + } + } + + // Compatibility + if let compatEditor = BAInjection.inject(BAInstallDataEditor.self) { + compatEditor.setIdentifier(customID) + compatEditor.save() + } else { + BALogger.error(domain: loggerDomain, message: "Failed to inject InstallDataEditor, cannot set compatibility user identifier") + } + + sendIdentifyEvent(customID: customID) + } + + public func trackPublicEvent(name: String, attributes: BatchEventAttributes?) throws { + guard eventNameValidationRegexp.matches(name) else { + throw BATSDKError.userInputValidation("Invalid event name ('\(name)'). Not tracking event.") + } + + var attributesCopy: BatchEventAttributes? = nil + + if let attributes { + // Copy the data so we're sure the dev does not change it between validation and tracking + attributesCopy = (attributes.copy() as! BatchEventAttributes) + let errors = validateEventAttributes(attributesCopy!) + guard errors.isEmpty else { + throw BATSDKError.userInputValidation("Failed to validate event attributes:\n\n\(errors.joined(separator: "\n"))\n\nNot tracking event.") + } + } + + BAInjection.inject(BATEventTracker.self)?.trackPublicEvent(name: name, attributes: attributesCopy) + } + + public func trackLocation(_ location: CLLocation) { + BAInjection.inject(BATEventTracker.self)?.trackLocation(location) + } + + public func validateEventAttributes(_ attributes: BatchEventAttributes) -> [String] { + return BATEventAttributesValidator(eventAttributes: attributes).computeValidationErrors() + } + + public func applyEditor(_ profileEditor: BATProfileEditor) { + let serializedEditOperations = BATProfileOperationsSerializer.serialize(profileEditor: profileEditor) + + BAInjection.inject(BATEventTracker.self)?.trackPrivateEvent(event: .profileDataChanged, parameters: serializedEditOperations, collapsable: false) + } + + /// Callback method when the project key has changed. + /// - Parameters: + /// - oldProjectKey: The old project key attached to this app + /// - newProjectKey: The new project key attached to this app + @objc(onProjectChanged:withNewKey:) + public func onProjectChanged(oldProjectKey: String?, newProjectKey _: String?) { + // Trigger migrations only the first time + if oldProjectKey == nil { + // Custom User ID migration (aka auto login) + if let customUserId = BAUserProfile.default().customIdentifier { + if BACoreCenter.instance().configuration.isMigrationDisabled(for: .customID) { + BALogger.debug(domain: loggerDomain, message: "Automatic custom id migration has been explicitly disabled.") + } else { + BALogger.public(domain: loggerDomain, message: "Automatic custom id migration.") + sendIdentifyEvent(customID: customUserId) + } + } + // Custom data migration + guard !BACoreCenter.instance().configuration.isMigrationDisabled(for: .customData) else { + BALogger.debug(domain: loggerDomain, message: "Automatic custom data migration has been explicitly disabled.") + return + } + BALogger.public(domain: loggerDomain, message: "Automatic custom data migration.") + migrateCustomData() + } + } + + /// Migrate installation's custom data to Profile. + private func migrateCustomData() { + // Instantiate profile editor + let profileEditor = BATProfileEditor() + + // Add custom language + if let customLanguage = BAUserProfile.default().language { + try? profileEditor.setLanguage(customLanguage) + } + + // Add custom region + if let customRegion = BAUserProfile.default().region { + try? profileEditor.setRegion(customRegion) + } + BAUserDataManager.sharedQueue().async { + let datasource = BAInjection.inject(BAUserDatasourceProtocol.self) + + // Add custom attributes + if let attributes = datasource?.attributes() { + attributes.forEach { (key: String, attribute: BAUserAttribute) in + let untypedKey = String(key.dropFirst(2)) + switch attribute.type { + case BAUserAttributeType.string: + if let stringValue = attribute.value as? String { + try? profileEditor.setCustom(stringAttribute: stringValue, forKey: untypedKey) + } + case BAUserAttributeType.longLong: + if let longLongValue = attribute.value as? Int64 { + try? profileEditor.setCustom(int64Attribute: longLongValue, forKey: untypedKey) + } + case BAUserAttributeType.double: + if let doubleValue = attribute.value as? Double { + try? profileEditor.setCustom(doubleAttribute: doubleValue, forKey: untypedKey) + } + case BAUserAttributeType.date: + if let dateValue = attribute.value as? NSDate { + try? profileEditor.setCustom(dateAttribute: dateValue, forKey: untypedKey) + } + case BAUserAttributeType.bool: + if let boolValue = attribute.value as? Bool { + try? profileEditor.setCustom(boolAttribute: boolValue, forKey: untypedKey) + } + case BAUserAttributeType.URL: + if let urlValue = attribute.value as? URL { + try? profileEditor.setCustom(urlAttribute: urlValue, forKey: untypedKey) + } + case .deleted: + break + @unknown default: break + } + } + } + // Add custom tags + if let tags = datasource?.tagCollections() { + tags.forEach { (key: String, value: Set) in + try? profileEditor.setCustom(stringArrayAttribute: Array(value), forKey: key) + } + } + // Serialize and send event + self.applyEditor(profileEditor) + } + } + + func sendIdentifyEvent(customID: String?) { + guard let installID = BatchUser.installationID else { + BALogger.error(domain: loggerDomain, message: "Could not track identify event: nil Installation ID") + return + } + + // Identifiers is split and not embedded as type inference for dictionaries + // compiles very slowly in Swift + let identifiers: [AnyHashable: Any] = [ + "custom_id": customID ?? NSNull(), + "install_id": installID, + ] + let eventParameters: [AnyHashable: Any] = [ + "identifiers": identifiers, + ] + BAInjection.inject(BATEventTracker.self)?.trackPrivateEvent(event: .profileIdentify, parameters: eventParameters, collapsable: false) + } +} diff --git a/Sources/Batch/Modules/Profile/BATEventAttributesSerializer.swift b/Sources/Batch/Modules/Profile/BATEventAttributesSerializer.swift new file mode 100644 index 0000000..1383552 --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATEventAttributesSerializer.swift @@ -0,0 +1,80 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +/// Serialize a BatchEventAttributes instance +/// No validation checks are made: make sure you call BATEventAttributesValidator on the _same_ BatchEventAttributes +@objc +@objcMembers +public class BATEventAttributesSerializer: NSObject { + /// Serialize an event + /// Throws a BATSDKError if a typecasting error happened, but this should not be possible if BatchEventAttributes and BATEventAttributesValidator + /// did their jobs properly + public static func serialize(eventAttributes: BatchEventAttributes) throws -> [AnyHashable: Any] { + var jsonParameters = [AnyHashable: Any]() + + if let label = eventAttributes._label { + jsonParameters["label"] = label + } + if let tags = eventAttributes._tags { + // Deduplicate and lowercase tags + jsonParameters["tags"] = Array(Set(tags.map { $0.lowercased() })) + } + + jsonParameters["attributes"] = try serializeAttributes(eventAttributes: eventAttributes) + + return jsonParameters + } + + private static func serializeAttributes(eventAttributes: BatchEventAttributes) throws -> [AnyHashable: Any] { + var jsonAttributes = [AnyHashable: Any]() + + for (attributeName, attributeValue) in eventAttributes._attributes { + let jsonKey = "\(attributeName).\(attributeValue.typeSuffix)" + switch attributeValue.type { + case .date, .string, .double, .integer, .bool: + jsonAttributes[jsonKey] = attributeValue.value + case .URL: + if let urlValue = attributeValue.value as? URL { + jsonAttributes[jsonKey] = urlValue.absoluteString + } else { + throw BATSDKError.sdkInternal(subcode: 1, reason: "attribute isn't an URL") + } + case .stringArray: + if let arrayValue = attributeValue.value as? [String] { + jsonAttributes[jsonKey] = arrayValue + } else { + throw BATSDKError.sdkInternal(subcode: 2, reason: "attribute isn't a string array") + } + + case .objectArray: + if let arrayValue = attributeValue.value as? [BatchEventAttributes] { + do { + jsonAttributes[jsonKey] = try arrayValue.map { try BATEventAttributesSerializer.serializeAttributes(eventAttributes: $0) } + } catch { + throw error + } + } else { + throw BATSDKError.sdkInternal(subcode: 3, reason: "attribute isn't a BatchEventAttributes array") + } + + case .object: + if let objectValue = attributeValue.value as? BatchEventAttributes { + do { + try jsonAttributes[jsonKey] = BATEventAttributesSerializer.serializeAttributes(eventAttributes: objectValue) + } catch { + throw error + } + } else { + throw BATSDKError.sdkInternal(subcode: 4, reason: "attribute isn't a BatchEventAttributes instance") + } + } + } + + return jsonAttributes + } +} diff --git a/Sources/Batch/Modules/Profile/BATEventAttributesValidator.swift b/Sources/Batch/Modules/Profile/BATEventAttributesValidator.swift new file mode 100644 index 0000000..c7847ed --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATEventAttributesValidator.swift @@ -0,0 +1,348 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +fileprivate enum Maximums { + static let labelLength = 200 + static let tagLength = 64 + static let tagsCount = 10 + static let attributesCount = 20 + static let urlLength = 2048 + static let stringLength = 200 + static let arrayItemsCount = 25 +} + +fileprivate enum Consts { + static let attributeNamePattern = "^[a-zA-Z0-9_]{1,30}$" +} + +fileprivate typealias ValidationErrorMessage = String + +fileprivate struct ValidationError { + let message: ValidationErrorMessage + var breadcrumbs: Breadcrumbs + + func render() -> String { + var attributePath = breadcrumbs.items.joined(separator: ".") + if attributePath.isEmpty { + attributePath = "" + } + return attributePath + ": " + message + } +} + +/// Breadcrumbs is similar to an ariane thread, keeping track of where we are in an object +/// For example, ["purchased_item", "name"] is the breadcrumb for a "name" attribute in a subobject +/// attribute named "purchased_item". +fileprivate struct Breadcrumbs { + var items: [String] + + func appending(_ item: String) -> Breadcrumbs { + return Breadcrumbs(items: items + [item]) + } + + func appending(index: Int) -> Breadcrumbs { + var mutatedItems = items + if let lastItem = mutatedItems.last { + mutatedItems[mutatedItems.endIndex - 1] = "\(lastItem)[\(index)]" + } + return Breadcrumbs(items: mutatedItems) + } + + var depth: Int { + items.count + } +} + +/// Class that validates that a BatchEventAttributes object is valid +struct BATEventAttributesValidator { + private let attributeNameRegexp: BATRegularExpression = .init(pattern: Consts.attributeNamePattern) + + let eventAttributes: BatchEventAttributes + + /// Validate the BatchEventAttributes instance and returns an array of errors + /// If there are none, the event data is valid + func computeValidationErrors() -> [String] { + return visitObject(eventAttributes: eventAttributes, breadcrumbs: Breadcrumbs(items: [])).map { $0.render() } + } + + // MARK: Visitors + + /// Check for errors in a BatchEventAttributes object. + /// The breadcrumb helps to build error messages for complex objects, it also acts as a depth counter + /// The errors are not returned using a throwing pattern as we want to accumulate them + private func visitObject(eventAttributes: BatchEventAttributes, breadcrumbs: Breadcrumbs) -> [ValidationError] { + // Quick bail on objects that are too deep + let depth = breadcrumbs.depth + guard depth <= 3 else { + return [ValidationError(message: "Object attributes cannot be nested in more than three levels", breadcrumbs: breadcrumbs)] + } + + // No attributes, no labels, no tags? Useless object, but quickly bail + if eventAttributes._attributes.isEmpty, eventAttributes._label == nil, eventAttributes._tags == nil { + return [] + } + + var errors: [ValidationError] = [] + + // Tags/Label validation + if depth > 0 { + // tags/label not allowed in subobjects + if eventAttributes._label != nil { + errors.append(ValidationError(message: "Labels are not allowed in sub-objects", breadcrumbs: breadcrumbs.appending("$label"))) + } + + if eventAttributes._tags != nil { + errors.append(ValidationError(message: "Tags are not allowed in sub-objects", breadcrumbs: breadcrumbs.appending("$tags"))) + } + } else { + // Root object, tags/label are allowed, check them + if let label = eventAttributes._label { + wrapAndMergeErrorMessages(visitLabel(label), breadcrumbs: breadcrumbs.appending("$label"), into: &errors) + } + + if let tags = eventAttributes._tags { + mergeErrors(visitTags(tags, breadcrumbs: breadcrumbs.appending("$tags")), into: &errors) + } + } + + let attributes = eventAttributes._attributes + + // Check for attributes count + if attributes.count > Maximums.attributesCount { + errors.append(ValidationError(message: "objects cannot hold more than \(Maximums.attributesCount) attributes", breadcrumbs: breadcrumbs)) + } + + for (attributeName, attributeValue) in attributes { + // Check for invalid attributes names + if let attributeNameError = visitAttributeName(attributeName, breadcrumbs) { + // If we encounter an error on the attribute name, do not try to parse it + errors.append(attributeNameError) + continue + } + + // Attribute name is now safe to print + let attributeBreadcrumbs = breadcrumbs.appending(attributeName) + mergeErrors(visitAttributeValue(attributeValue, attributeBreadcrumbs), into: &errors) + } + + return errors + } + + private func visitAttributeName(_ name: String, _ breadcrumbs: Breadcrumbs) -> ValidationError? { + let baseError = "invalid attribute name '\(name)':" + if name != name.lowercased() { + // We should have lowercased this, someone is doing something nasty~ + return ValidationError(message: "\(baseError) object has been tampered with", breadcrumbs: breadcrumbs) + } + + if attributeNameRegexp.regexpFailedToInitialize { + return ValidationError(message: "\(baseError) internal error", breadcrumbs: breadcrumbs) + } + + if !attributeNameRegexp.matches(name) { + return ValidationError(message: "\(baseError) please make sure that the key is made of letters, underscores and numbers only (a-zA-Z0-9_). It also can't be longer than 30 characters", breadcrumbs: breadcrumbs) + } + + return nil + } + + private func visitAttributeValue(_ attribute: BATTypedEventAttribute, _ breadcrumbs: Breadcrumbs) -> [ValidationError] { + var errors: [ValidationError] = [] + + let genericTypecastError = ValidationError(message: "attribute is not of the right underlying type. this is an internal error and should be reported", breadcrumbs: breadcrumbs) + + switch attribute.type { + case .URL: + if let url = attribute.value as? URL { + mergeError(visitAttributeURLValue(url, breadcrumbs), into: &errors) + } else { + errors.append(genericTypecastError) + } + case .string: + if let stringValue = attribute.value as? String { + mergeError(visitAttributeStringValue(stringValue, breadcrumbs), into: &errors) + } else { + errors.append(genericTypecastError) + } + case .double, .integer, .bool, .date: + if !(attribute.value is NSNumber) { + errors.append(genericTypecastError) + } + case .objectArray, .stringArray: + if let anyArrayValue = attribute.value as? [Any] { + if let baseArrayError = visitAttributeArrayValueBase(anyArrayValue, breadcrumbs) { + errors.append(baseArrayError) + } else { + // Only continue if the array passed base validation + if attribute.type == .objectArray { + if let objectArrayValue = attribute.value as? [BatchEventAttributes] { + mergeErrors(visitAttributeObjectArrayValue(objectArrayValue, breadcrumbs), into: &errors) + } else { + errors.append(genericTypecastError) + } + } else if attribute.type == .stringArray { + if let stringArrayValue = attribute.value as? [String] { + mergeErrors(visitAttributeStringArrayValue(stringArrayValue, breadcrumbs), into: &errors) + } else { + errors.append(genericTypecastError) + } + } + } + } else { + errors.append(genericTypecastError) + } + case .object: + if let objectValue = attribute.value as? BatchEventAttributes { + mergeErrors(visitObject(eventAttributes: objectValue, breadcrumbs: breadcrumbs), into: &errors) + } else { + errors.append(genericTypecastError) + } + } + + return errors + } + + private func visitAttributeArrayValueBase(_ value: [Any], _ breadcrumbs: Breadcrumbs) -> ValidationError? { + let depth = breadcrumbs.depth + guard depth <= 3 else { + return ValidationError(message: "array attributes cannot be nested in more than three levels", breadcrumbs: breadcrumbs) + } + + if value.count > Maximums.arrayItemsCount { + return ValidationError(message: "array attributes cannot have more than \(Maximums.arrayItemsCount) elements", breadcrumbs: breadcrumbs) + } + + return nil + } + + private func visitAttributeStringArrayValue(_ array: [String], _ breadcrumbs: Breadcrumbs) -> [ValidationError] { + var errors: [ValidationError] = [] + + for i in array.indices { + let value = array[i] + let itemBreadcrumbs = breadcrumbs.appending(index: i) + + mergeError(visitAttributeStringValue(value, itemBreadcrumbs), into: &errors) + } + + return errors + } + + private func visitAttributeObjectArrayValue(_ array: [BatchEventAttributes], _ breadcrumbs: Breadcrumbs) -> [ValidationError] { + var errors: [ValidationError] = [] + + for i in array.indices { + let value = array[i] + let itemBreadcrumbs = breadcrumbs.appending(index: i) + + mergeErrors(visitObject(eventAttributes: value, breadcrumbs: itemBreadcrumbs), into: &errors) + } + + return errors + } + + private func visitAttributeStringValue(_ value: String, _ breadcrumbs: Breadcrumbs) -> ValidationError? { + if value.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { + return ValidationError(message: "string attribute cannot be empty or made of whitespace", breadcrumbs: breadcrumbs) + } + + if value.count > Maximums.stringLength { + return ValidationError(message: "string attribute cannot be longer than \(Maximums.stringLength) characters", breadcrumbs: breadcrumbs) + } + + if value.contains("\n") { + return ValidationError(message: "string attribute cannot be multiline", breadcrumbs: breadcrumbs) + } + + return nil + } + + private func visitAttributeURLValue(_ value: URL, _ breadcrumbs: Breadcrumbs) -> ValidationError? { + if value.absoluteString.count > Maximums.urlLength { + return ValidationError(message: "URL attributes cannot be longer than \(Maximums.urlLength) characters", breadcrumbs: breadcrumbs) + } + + if value.scheme == nil || value.host == nil { + return ValidationError(message: "URL attributes must follow the format 'scheme://[authority][path][?query][#fragment]'", breadcrumbs: breadcrumbs) + } + + return nil + } + + private func visitLabel(_ label: String) -> [ValidationErrorMessage] { + var errors: [String] = [] + + if label.count > Maximums.labelLength { + errors.append("cannot be longer than 200 characters") + } + if label.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { + errors.append("cannot be empty or only made of whitespace") + } + if label.contains("\n") { + errors.append("cannot be multiline") + } + + return errors + } + + private func visitTags(_ tags: [String], breadcrumbs: Breadcrumbs) -> [ValidationError] { + var errors: [ValidationError] = [] + + if tags.count > Maximums.tagsCount { + errors.append(ValidationError(message: "must not contain more than \(Maximums.tagsCount) values", breadcrumbs: breadcrumbs)) + } + + for index in tags.indices { + if let error = visitTag(tags[index]) { + errors.append(ValidationError(message: error, breadcrumbs: breadcrumbs.appending(index: index))) + } + } + + return errors + } + + private func visitTag(_ tag: String) -> ValidationErrorMessage? { + if tag.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { + return "tag cannot be empty or made of whitespace" + } + + if tag.count > Maximums.tagLength { + return "tag cannot be longer than \(Maximums.tagLength) characters" + } + + if tag.contains("\n") { + return "tag cannot be multiline" + } + + return nil + } + + // MARK: Error helpers + + private func wrapErrorMessages(_ messages: [ValidationErrorMessage], breadcrumbs: Breadcrumbs) -> [ValidationError] { + return messages.map { message in + return ValidationError(message: message, breadcrumbs: breadcrumbs) + } + } + + private func wrapAndMergeErrorMessages(_ messages: [ValidationErrorMessage], breadcrumbs: Breadcrumbs, into accumulator: inout [ValidationError]) { + accumulator.append(contentsOf: messages.map { message in + return ValidationError(message: message, breadcrumbs: breadcrumbs) + }) + } + + private func mergeError(_ error: ValidationError?, into accumulator: inout [ValidationError]) { + if let error { + accumulator.append(error) + } + } + + private func mergeErrors(_ errors: [ValidationError], into accumulator: inout [ValidationError]) { + accumulator.append(contentsOf: errors) + } +} diff --git a/Sources/Batch/Modules/Profile/BATProfileDataValidators.swift b/Sources/Batch/Modules/Profile/BATProfileDataValidators.swift new file mode 100644 index 0000000..c6bb27e --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATProfileDataValidators.swift @@ -0,0 +1,52 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +@objc +@objcMembers +public class BATProfileDataValidators: NSObject { + static let loggingDomain = "ProfileDataValidator" + // \r\n\t is \s but for some reason \S doesn't validate those in a negation so we explicitly use those + static let emailValidationRegexpPattern = "^[^@\\r\\n\\t]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$" + + public static let emailMaxLength = 256 + public static let customIDMaxLength = 1024 + + public static func isValidEmail(_ email: String) -> Bool { + let regexp = BATRegularExpression(pattern: emailValidationRegexpPattern) + guard regexp.regexpFailedToInitialize == false else { + BALogger.debug(domain: loggingDomain, message: "Email regexp unavailable") + return false + } + + return regexp.matches(email) + } + + public static func isEmailTooLong(_ email: String) -> Bool { + return email.count > emailMaxLength + } + + public static func isCustomIDAllowed(_ customID: String) -> Bool { + if customID.contains("\n") { + return false + } + + if customID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return false + } + + return true + } + + public static func isCustomIDTooLong(_ customID: String) -> Bool { + if customID.count > customIDMaxLength { + return true + } + + return false + } +} diff --git a/Sources/Batch/Modules/Profile/BATProfileEditor.swift b/Sources/Batch/Modules/Profile/BATProfileEditor.swift new file mode 100644 index 0000000..cb4c7c2 --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATProfileEditor.swift @@ -0,0 +1,480 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +fileprivate enum Maximums { + static let stringArrayItems = 25 + static let stringLength = 64 + static let emailLength = 256 + static let urlLength = 2048 +} + +fileprivate enum Consts { + static let attributeNamePattern = "^[a-zA-Z0-9_]{1,30}$" + static let emailAddressPattern = "^[^@\\s]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$" +} + +/// Protocol that exposes BATProfileEditor's state, so that it can be serialized +protocol BATSerializableProfileEditorProtocol { + var email: (any BATProfileAttributeOperation)? { get } + + var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState? { get } + + var language: (any BATProfileAttributeOperation)? { get } + + var region: (any BATProfileAttributeOperation)? { get } + + var customAttributes: [String: any BATProfileAttributeOperation] { get } +} + +/// Protocol that exposes BATProfileEditor's setters that are available in the legacy install data world +@objc +public protocol BATInstallDataEditorCompatibilityProtocol { + @objc + func setLanguage(_ value: String?) throws + + @objc + func setRegion(_ value: String?) throws + + @objc + func add(value: String, toArray attributeKey: String) throws + + @objc + func remove(value: String, fromArray attributeKey: String) throws + + @objc(setCustomStringArrayAttribute:forKey:error:) + func setCustom(stringArrayAttribute: [String], forKey attributeKey: String) throws + + @objc(setCustomBoolAttribute:forKey:error:) + func setCustom(boolAttribute: Bool, forKey attributeKey: String) throws + + @objc(setCustomInt64Attribute:forKey:error:) + func setCustom(int64Attribute: Int64, forKey attributeKey: String) throws + + @objc(setCustomDoubleAttribute:forKey:error:) + func setCustom(doubleAttribute: Double, forKey attributeKey: String) throws + + @objc(setCustomStringAttribute:forKey:error:) + func setCustom(stringAttribute: String, forKey attributeKey: String) throws + + @objc(setCustomDateAttribute:forKey:error:) + func setCustom(dateAttribute: NSDate, forKey attributeKey: String) throws + + @objc(setCustomURLAttribute:forKey:error:) + func setCustom(urlAttribute: URL, forKey attributeKey: String) throws + + @objc + func deleteCustomAttribute(forKey attributeKey: String) throws + + @objc + func consume() +} + +/// BATProfileEditor holds multiple profile update operation +/// Serialization occurs in another class +@objc +public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, NSCopying { + private let attributeNameRegexp: BATRegularExpression = .init(pattern: Consts.attributeNamePattern) + private let emailAddressRegexp: BATRegularExpression = .init(pattern: Consts.emailAddressPattern) + + private(set) var email: (any BATProfileAttributeOperation)? + + private(set) var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState? + + private(set) var language: (any BATProfileAttributeOperation)? + + private(set) var region: (any BATProfileAttributeOperation)? + + private(set) var customAttributes: [String: any BATProfileAttributeOperation] = [:] + + // Delegate that will be informed of all operations so that it can perform them + // on the install data. + // Its methods are only called if validation passed. + private var compatibilityDelegate: BATInstallDataEditorCompatibilityProtocol? + + @objc + public func enableInstallCompatibility() { + if let compatibility = BATProfileInstallDataCompatibility() { + compatibilityDelegate = compatibility + } else { + BALogger.error(domain: "ProfileEditor", message: "Could not instanciate compatibility delegate. Install data will NOT be modified.") + } + } + + // Has this editor been consumed? + // If yes, it cannot be used to edit further attributes + @objc + public var consumed: Bool = false + + @objc + public func setEmail(_ value: String?) throws { + try checkIfConsumed() + + if let value { + let baseError = "Cannot set email address:" + + if !canSetEmail() { + throw BatchProfileError(code: .editorInvalidValue, reason: "Emails cannot be set on a profile if it has not been identified first. Please call 'BatchProfile.idenfity()' with a non nil value beforehand.") + } + + if value.count > Maximums.emailLength { + throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) address cannot be longer than \(Maximums.emailLength) characters") + } + + if !emailAddressRegexp.matches(value) { + throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) invalid address") + } + + email = BATProfileAttributeSetOperation(type: .string, value: value) + } else { + email = BATProfileAttributeDeleteOperation() + } + } + + @objc + public func setEmailMarketingSubscriptionState(_ value: BATProfileEditorEmailSubscriptionState) { + do { + try checkIfConsumed() + emailMarketingSubscription = value + } catch { + // Do nothing + } + } + + @objc + public func setLanguage(_ value: String?) throws { + try checkIfConsumed() + + if let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) { + let size = value.count + if size < 2 || size > 15 { + throw BatchProfileError(code: .editorInvalidValue, reason: "Cannot set language: language code must be at least two chars and less than 15") + } + + language = BATProfileAttributeSetOperation(type: .string, value: value) + } else { + language = BATProfileAttributeDeleteOperation() + } + + try? compatibilityDelegate?.setLanguage(value) + } + + @objc + public func setRegion(_ value: String?) throws { + try checkIfConsumed() + + if let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) { + let size = value.count + if size < 2 || size > 15 { + throw BatchProfileError(code: .editorInvalidValue, reason: "Cannot set region: region code must be at least two chars and less than 15") + } + + region = BATProfileAttributeSetOperation(type: .string, value: value) + } else { + region = BATProfileAttributeDeleteOperation() + } + + try? compatibilityDelegate?.setRegion(value) + } + + @objc + public func add(value: String, toArray attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + try validateStringValue(value) + + // Check if we have an existing attribute for that key. + // If so: + // - It is an array: add to it after checking that it does not go over the limit + // - It is a partial array operation: append + // - It is missing or something else than an array: overwrite it + let existingOperation = customAttributes[targetAttributeKey] + + if let existingOperation = existingOperation as? BATProfileAttributeSetOperation<[String]>, existingOperation.type == .array { + var updatedArray = existingOperation.value + updatedArray.append(value) + try validateStringArray(updatedArray) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation<[String]>(type: .array, value: updatedArray) + } else if let existingOperation = existingOperation as? BATProfileAttributePartialArrayUpdateOperation { + var updatedPartialUpdate = existingOperation + updatedPartialUpdate.itemsToAdd.append(value) + try validateParialUpdate(updatedPartialUpdate) + customAttributes[targetAttributeKey] = updatedPartialUpdate + } else { + customAttributes[targetAttributeKey] = BATProfileAttributePartialArrayUpdateOperation(itemsToAdd: [value], itemsToRemove: []) + } + + try? compatibilityDelegate?.add(value: value, toArray: attributeKey) + } + + @objc + public func remove(value: String, fromArray attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + try validateStringValue(value) + + // Check if we have an existing attribute for that key. + // If so: + // - It is an array: remove from it. If the array is empty, remove the operation. + // - It is a partial array operation: add the removal to it + // - It is missing or something else than an array: overwrite it + let existingOperation = customAttributes[targetAttributeKey] + + if let existingOperation = existingOperation as? BATProfileAttributeSetOperation<[String]>, existingOperation.type == .array { + var updatedArray = existingOperation.value + updatedArray.removeAll { $0 == value } + + if updatedArray.count == 0 { + customAttributes.removeValue(forKey: targetAttributeKey) + } else { + try validateStringArray(updatedArray) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation<[String]>(type: .array, value: updatedArray) + } + } else if let existingOperation = existingOperation as? BATProfileAttributePartialArrayUpdateOperation { + var updatedPartialUpdate = existingOperation + updatedPartialUpdate.itemsToRemove.append(value) + try validateParialUpdate(updatedPartialUpdate) + customAttributes[targetAttributeKey] = updatedPartialUpdate + } else { + customAttributes[targetAttributeKey] = BATProfileAttributePartialArrayUpdateOperation(itemsToAdd: [], itemsToRemove: [value]) + } + + try? compatibilityDelegate?.remove(value: value, fromArray: attributeKey) + } + + @objc(setCustomStringArrayAttribute:forKey:error:) + public func setCustom(stringArrayAttribute: [String], forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + try validateStringArray(stringArrayAttribute) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation<[String]>(type: .array, value: stringArrayAttribute) + + try? compatibilityDelegate?.setCustom(stringArrayAttribute: stringArrayAttribute, forKey: attributeKey) + } + + @objc(setCustomBoolAttribute:forKey:error:) + public func setCustom(boolAttribute: Bool, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + // Bools/Numbers need to be wrapped in a NSNumber as when serializing this will be bridged + // to an Obj-C NSDictionary + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .bool, value: NSNumber(value: boolAttribute)) + + try? compatibilityDelegate?.setCustom(boolAttribute: boolAttribute, forKey: attributeKey) + } + + @objc(setCustomInt64Attribute:forKey:error:) + public func setCustom(int64Attribute: Int64, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .longLong, value: NSNumber(value: int64Attribute)) + + try? compatibilityDelegate?.setCustom(int64Attribute: int64Attribute, forKey: attributeKey) + } + + @objc(setCustomDoubleAttribute:forKey:error:) + public func setCustom(doubleAttribute: Double, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .double, value: NSNumber(value: doubleAttribute)) + + try? compatibilityDelegate?.setCustom(doubleAttribute: doubleAttribute, forKey: attributeKey) + } + + @objc(setCustomStringAttribute:forKey:error:) + public func setCustom(stringAttribute: String, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + try validateStringValue(stringAttribute) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .string, value: stringAttribute as AnyObject) + + try? compatibilityDelegate?.setCustom(stringAttribute: stringAttribute, forKey: attributeKey) + } + + @objc(setCustomDateAttribute:forKey:error:) + public func setCustom(dateAttribute: NSDate, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .date, value: NSNumber(value: floor(dateAttribute.timeIntervalSince1970 * 1000))) + + try? compatibilityDelegate?.setCustom(dateAttribute: dateAttribute, forKey: attributeKey) + } + + @objc(setCustomURLAttribute:forKey:error:) + public func setCustom(urlAttribute: URL, forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + + guard urlAttribute.absoluteString.count <= Maximums.urlLength else { + throw BatchProfileError(code: .editorInvalidValue, reason: "URL attributes can't be longer than \(Maximums.urlLength) characters") + } + + guard urlAttribute.scheme != nil, urlAttribute.host != nil else { + throw BatchProfileError(code: .editorInvalidValue, reason: "URL attributes must be of format 'scheme://[authority][path][?query][#fragment]'") + } + + customAttributes[targetAttributeKey] = BATProfileAttributeSetOperation(type: .url, value: urlAttribute.absoluteString as NSString) + + try? compatibilityDelegate?.setCustom(urlAttribute: urlAttribute, forKey: attributeKey) + } + + @objc + public func deleteCustomAttribute(forKey attributeKey: String) throws { + try checkIfConsumed() + + let targetAttributeKey = try validateAndNormalizeName(attributeKey) + customAttributes[targetAttributeKey] = BATProfileAttributeDeleteOperation() + + try? compatibilityDelegate?.deleteCustomAttribute(forKey: attributeKey) + } + + @objc + public func consume() { + if consumed { + return + } + consumed = true + compatibilityDelegate?.consume() + } + + func validateAndNormalizeName(_ name: String) throws -> String { + let normalizedName = name.lowercased() + let baseError = "invalid attribute name '\(name)':" + + if !attributeNameRegexp.matches(name) { + throw BatchProfileError(code: .editorInvalidKey, reason: "\(baseError) please make sure that the key is made of letters, underscores and numbers only (a-zA-Z0-9_). It also can't be longer than 30 characters") + } + + return normalizedName + } + + func validateStringValue(_ value: String) throws { + if value.count > Maximums.stringLength { + throw BatchProfileError(code: .editorInvalidValue, reason: "invalid attribute value: strings cannot be longer than \(Maximums.stringLength) characters") + } + } + + func validateStringArray(_ values: [String]) throws { + if values.count > Maximums.stringArrayItems { + throw BatchProfileError(code: .editorInvalidValue, reason: "invalid attribute value: string arrays cannot contain more than \(Maximums.stringArrayItems) elements") + } + + for value in values { + try validateStringValue(value) + } + } + + func validateParialUpdate(_ partialUpdate: BATProfileAttributePartialArrayUpdateOperation) throws { + if partialUpdate.itemsToAdd.count > Maximums.stringArrayItems || partialUpdate.itemsToRemove.count > Maximums.stringArrayItems { + throw BatchProfileError(code: .editorInvalidValue, reason: "a partial array operation cannot add or remove more than \(Maximums.stringArrayItems) at once") + } + } + + func checkIfConsumed() throws { + if consumed { + throw BatchProfileError(code: .editorConsumed, reason: "a BatchProfileEditor instance cannot be saved more than once. Please acquire a new instance") + } + } + + public func copy(with _: NSZone? = nil) -> Any { + let copy = BATProfileEditor() + copy.email = self.email + copy.emailMarketingSubscription = self.emailMarketingSubscription + copy.language = self.language + copy.region = self.region + copy.customAttributes = self.customAttributes + return copy + } + + func canSetEmail() -> Bool { + // We can only set an email if the user is logged in + // This method is exposed for testing purposes + return BAUserProfile.default().customIdentifier != nil + } +} + +/// Represents an operation on a profile attribute. +/// If you do not want to do anything on an attribute, store nil rather than an instance of this protocol +protocol BATProfileAttributeOperation { + associatedtype ValueType + + var type: BATProfileAttributeOperationType { get } + // All values should be ready to be serialized as is in json + // for the server event. + // If they are not, please document it in your implementation + + var value: ValueType { get } + + // Suffix to add to typed keys + var keySuffix: String { get } +} + +enum BATProfileAttributeOperationType: String { + case bool = "b" + case longLong = "i" + case double = "f" + case string = "s" + case date = "t" + case url = "u" + case array = "a" + case delete = "x" +} + +public struct BATProfileAttributeSetOperation: BATProfileAttributeOperation { + typealias ValueType = T + + let type: BATProfileAttributeOperationType + let value: ValueType + + // Prefix to add to typed keys + var keySuffix: String { + if self.type == .delete { + return "" + } + return "." + self.type.rawValue + } +} + +public struct BATProfileAttributeDeleteOperation: BATProfileAttributeOperation { + typealias ValueType = NSNull + + let type = BATProfileAttributeOperationType.delete + // Using NSNull is important as this is what we'll use for serialization + // in the NSDictionary + let value: ValueType = NSNull() + + let keySuffix: String = "" +} + +public struct BATProfileAttributePartialArrayUpdateOperation: BATProfileAttributeOperation { + typealias ValueType = NSNull + + let type = BATProfileAttributeOperationType.array + /// Do not read value on this type, it is not representative of its real serialization + let value: ValueType = NSNull() + let keySuffix: String = "." + BATProfileAttributeOperationType.array.rawValue + + var itemsToAdd: [String] = [] + var itemsToRemove: [String] = [] +} + +/// Email subscription state. This is already defined in BatchProfile.h, but we cannot reexpose +/// an @objc method with a parameter from a public header, as this creates an import loop. +@objc +public enum BATProfileEditorEmailSubscriptionState: UInt { + case subscribed = 0 + case unsubscribed = 1 +} diff --git a/Sources/Batch/Modules/Profile/BATProfileInstallDataCompatibility.swift b/Sources/Batch/Modules/Profile/BATProfileInstallDataCompatibility.swift new file mode 100644 index 0000000..bf00851 --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATProfileInstallDataCompatibility.swift @@ -0,0 +1,78 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +class BATProfileInstallDataCompatibility: BATInstallDataEditorCompatibilityProtocol { + let installDataEditor: BAInstallDataEditor! + + init?() { + guard let injectedEditor = BAInjection.inject(BAInstallDataEditor.self) else { + return nil + } + self.installDataEditor = injectedEditor + } + + func setLanguage(_ value: String?) throws { + installDataEditor.setLanguage(value) + } + + func setRegion(_ value: String?) throws { + installDataEditor.setRegion(value) + } + + func add(value: String, toArray attributeKey: String) throws { + installDataEditor.addTag(value, inCollection: attributeKey) + } + + func remove(value: String, fromArray attributeKey: String) throws { + installDataEditor.removeTag(value, fromCollection: attributeKey) + } + + func setCustom(stringArrayAttribute: [String], forKey attributeKey: String) throws { + installDataEditor.clearTagCollection(attributeKey) + for tag in stringArrayAttribute { + installDataEditor.addTag(tag, inCollection: attributeKey) + } + } + + func setCustom(boolAttribute: Bool, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(boolAttribute, forKey: attributeKey) + } + + func setCustom(int64Attribute: Int64, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(int64Attribute, forKey: attributeKey) + } + + func setCustom(doubleAttribute: Double, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(doubleAttribute, forKey: attributeKey) + } + + func setCustom(stringAttribute: String, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(stringAttribute, forKey: attributeKey) + } + + func setCustom(dateAttribute: NSDate, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(dateAttribute as Date, forKey: attributeKey) + } + + func setCustom(urlAttribute: URL, forKey attributeKey: String) throws { + try? installDataEditor.setAttribute(urlAttribute, forKey: attributeKey) + } + + func deleteCustomAttribute(forKey attributeKey: String) throws { + // We do both: removeAttribute and clearTagCollection since we don't know + // the type for the install-based compat, so we don't know if we should remove + // a tag collection or an attribute. + // This will not do anything if the attribute or collection doesn't exist. + installDataEditor.removeAttribute(forKey: attributeKey) + installDataEditor.clearTagCollection(attributeKey) + } + + func consume() { + installDataEditor.save() + } +} diff --git a/Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift b/Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift new file mode 100644 index 0000000..8452603 --- /dev/null +++ b/Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift @@ -0,0 +1,84 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +/// Serialize operations described in a BATProfileEditor +/// No validation checks are made: BATProfileEditor does them +class BATProfileOperationsSerializer: NSObject { + /// Serialize a Profile Editor + static func serialize(profileEditor: BATSerializableProfileEditorProtocol) -> [AnyHashable: Any] { + var jsonParameters = [AnyHashable: Any]() + + if let email = profileEditor.email { + jsonParameters["email"] = email.value + } + + if let emailMarketingSubscription = profileEditor.emailMarketingSubscription { + let serializedValue: String + switch emailMarketingSubscription { + case .subscribed: + serializedValue = "subscribed" + case .unsubscribed: + serializedValue = "unsubscribed" + } + jsonParameters["email_marketing"] = serializedValue + } + + if let language = profileEditor.language { + jsonParameters["language"] = language.value + } + + if let region = profileEditor.region { + jsonParameters["region"] = region.value + } + + if !profileEditor.customAttributes.isEmpty { + jsonParameters["custom_attributes"] = serializeAttributes(profileEditor: profileEditor) + } + + return jsonParameters + } + + private static func serializeAttributes(profileEditor: BATSerializableProfileEditorProtocol) -> [AnyHashable: Any] { + var jsonAttributes = [AnyHashable: Any]() + + for (attributeName, attributeOperation) in profileEditor.customAttributes { + let jsonKey = "\(attributeName)\(attributeOperation.keySuffix)" + + // Partial updates need to be serialized differently + if let partialArrayOperation = attributeOperation as? BATProfileAttributePartialArrayUpdateOperation { + // If the func returns nil, it should not be serialized + jsonAttributes[jsonKey] = serializePartialArrayUpdate(partialArrayOperation) + } else { + // All values should be serializable directly + jsonAttributes[jsonKey] = attributeOperation.value + } + } + + return jsonAttributes + } + + private static func serializePartialArrayUpdate(_ operation: BATProfileAttributePartialArrayUpdateOperation) -> [AnyHashable: Any]? { + var json = [AnyHashable: Any]() + + if operation.itemsToAdd.count > 0 { + json["$add"] = operation.itemsToAdd + } + + if operation.itemsToRemove.count > 0 { + json["$remove"] = operation.itemsToRemove + } + + // Turns out we had nothing to do. + // ProfileEditor could handle that but we don't care + if json.isEmpty { + return nil + } + + return json + } +} diff --git a/Sources/Batch/Modules/Profile/BatchProfileError+Init.swift b/Sources/Batch/Modules/Profile/BatchProfileError+Init.swift new file mode 100644 index 0000000..dbbeb30 --- /dev/null +++ b/Sources/Batch/Modules/Profile/BatchProfileError+Init.swift @@ -0,0 +1,15 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +/// Convinence initializer for the generated BatchProfileError that makes it less verbose +/// to add a description +extension BatchProfileError { + init(code: BatchProfileError.Code, reason: String) { + self.init(code, userInfo: [NSLocalizedDescriptionKey: reason]) + } +} diff --git a/Sources/Batch/Modules/Push/BAPushCenter.h b/Sources/Batch/Modules/Push/BAPushCenter.h index 95a559d..5101f40 100644 --- a/Sources/Batch/Modules/Push/BAPushCenter.h +++ b/Sources/Batch/Modules/Push/BAPushCenter.h @@ -88,12 +88,13 @@ extern NSString *const kBATPushOpenedNotificationOriginatesFromAppDelegate; /** Ask for the permission to display notifications */ -- (void)requestNotificationAuthorization; +- (void)requestNotificationAuthorizationWithCompletionHandler:(void (^)(BOOL granted, NSError *error))completionHandler; /** Ask for the permission to display provisional notifications */ -- (void)requestProvisionalNotificationAuthorization; +- (void)requestProvisionalNotificationAuthorizationWithCompletionHandler:(void (^)(BOOL granted, + NSError *error))completionHandler; /** Equivalent to [UIApplication.sharedApplication registerForRemoteNotifications] @@ -105,17 +106,6 @@ extern NSString *const kBATPushOpenedNotificationOriginatesFromAppDelegate; */ - (void)openSystemNotificationSettings; -/*! - @method setNotificationsCategories: - @abstract Set the notification action categories to iOS. - @discussion You should call this every time your app starts - @param categories : A set of UIUserNotificationCategory or UNNotificationCategory instances that define the groups of - actions a notification may include. If you try to register UIUserNotificationCategory instances on iOS 10, Batch will - automatically do a best effort conversion to UNNotificationCategory. If you don't want this behaviour, please use the - standard UIApplication methods. - */ -+ (void)setNotificationsCategories:(NSSet *)categories; - /*! @method clearBadge @abstract Clear the application's badge on the homescreen. @@ -131,14 +121,6 @@ extern NSString *const kBATPushOpenedNotificationOriginatesFromAppDelegate; */ + (void)dismissNotifications; -/*! - @method enableAutomaticDeeplinkHandling: - @abstract Set whether Batch Push should automatically try to handle deeplinks - @discussion By default, this is set to YES. You need to call everytime your app is restarted, this option is not - persisted. - */ -+ (void)enableAutomaticDeeplinkHandling:(BOOL)handleDeeplinks; - /*! @method deeplinkFromUserInfo: @abstract Get Batch Push's deeplink from a notification's userInfo. @@ -173,43 +155,6 @@ extern NSString *const kBATPushOpenedNotificationOriginatesFromAppDelegate; */ + (BOOL)isBatchPush:(NSDictionary *)userInfo; -/*! - @method handleNotification - @abstract Make Batch process a notification. You should call this method in "application:didReceiveRemoteNotification:" - or "application:didReceiveRemoteNotification:fetchCompletionHandler:". - @warning If you didn't call "disableAutomaticIntegration", this method will have no effect. If you called it but don't - implement this method, Batch's push features will NOT work. - @param userInfo : The untouched "userInfo" NSDictionary argument given to you in the application delegate method. - */ -+ (void)handleNotification:(NSDictionary *)userInfo; - -/*! - @method handleNotification - @abstract Make Batch process a notification action. You should call this method in - "application:handleActionWithIdentifier:forRemoteNotification:completionHandler:" or - "application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:". - @warning If you didn't call "disableAutomaticIntegration", this method will have no effect. If you called it but don't - implement this method, Batch's push features will NOT work. - @param userInfo : The untouched "userInfo" NSDictionary argument given to you in the application delegate method. - @param identifier : The action's identifier. Used for tracking purposes: it can match your raw action name, or be a - more user-friendly string; - */ -+ (void)handleNotification:(NSDictionary *)userInfo actionIdentifier:(NSString *)identifier; - -/*! - @method handleNotification - @abstract Make Batch process the user notification settings change. You should call this method in - "application:didRegisterUserNotificationSettings:". - @warning If you didn't call "disableAutomaticIntegration", this method will have no effect. If you called it but don't - implement this method, Batch's push features will NOT work. - @param notificationSettings : The untouched "notificationSettings" UIUserNotificationSettings* argument given to you in - the application delegate method. - */ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -+ (void)handleRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; -#pragma clang diagnostic pop - /** Make Batch process a foreground notification. You should call this method if you set your own UNUserNotificationCenterDelegate, in userNotificationCenter:willPresentNotification:withCompletionHandler: diff --git a/Sources/Batch/Modules/Push/BAPushCenter.m b/Sources/Batch/Modules/Push/BAPushCenter.m index 41a95d5..a7f4c2d 100644 --- a/Sources/Batch/Modules/Push/BAPushCenter.m +++ b/Sources/Batch/Modules/Push/BAPushCenter.m @@ -109,14 +109,8 @@ + (void)clearBadge { [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; } -// Clear the app's notifications in the notification center. Also clears your badge. + (void)dismissNotifications { - [[UIApplication sharedApplication] setApplicationIconBadgeNumber:1]; - [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; -} - -+ (void)enableAutomaticDeeplinkHandling:(BOOL)handleDeeplinks { - [[BAPushCenter instance] setHandleDeeplinks:handleDeeplinks]; + [[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications]; } + (NSString *)deeplinkFromUserInfo:(NSDictionary *)userInfo { @@ -142,21 +136,6 @@ + (BOOL)isBatchPush:(NSDictionary *)userInfo { return [[BAPushCenter instance] isBatchPush:userInfo]; } -+ (void)handleNotification:(NSDictionary *)userInfo { - [[BAPushCenter instance] handleNotification:userInfo]; -} - -+ (void)handleNotification:(NSDictionary *)userInfo actionIdentifier:(NSString *)identifier { - [[BAPushCenter instance] handleNotification:userInfo actionIdentifier:identifier]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -+ (void)handleRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - [[BAPushCenter instance] handleRegisterUserNotificationSettings:notificationSettings]; -} -#pragma clang diagnostic pop - #pragma mark - #pragma mark Private methods @@ -175,13 +154,10 @@ + (void)applicationDidFinishLaunchingNotification:(NSNotification *)notification @"'application:didFinishLaunchingWithOptions:'. This can cause erratic behaviour " @"with opens, mobile landings and other features. Please use " @"BatchUNUserNotificationCenterDelegate or implement your own."]; - if (@available(iOS 13.0, *)) { - if ([BAApplicationLifecycle applicationUsesUIScene]) { - [BALogger - publicForDomain:@"Push" - message:@"⚠️ App is using UIScene without a UNUserNotificationCenterDelegate: " - @"Direct Opens, Mobile Landings, Deeplinks will not work."]; - } + if ([BAApplicationLifecycle applicationUsesUIScene]) { + [BALogger publicForDomain:@"Push" + message:@"⚠️ App is using UIScene without a UNUserNotificationCenterDelegate: " + @"Direct Opens, Mobile Landings, Deeplinks will not work."]; } } } @@ -282,20 +258,25 @@ - (void)setRemoteNotificationTypes:(BatchNotificationType)type { [self setNotificationType:type]; } -- (void)requestNotificationAuthorization { +- (void)requestNotificationAuthorizationWithCompletionHandler:(void (^)(BOOL granted, + NSError *error))completionHandler { + [self setShouldAutomaticallyRetreivePushToken:YES]; + id pushSystemHelper = [BAInjection injectProtocol:@protocol(BAPushSystemHelperProtocol)]; - [pushSystemHelper registerForRemoteNotificationsTypes:[self notificationType] - providesNotificationSettings:self.supportsAppNotificationSettings]; - [self setShouldAutomaticallyRetreivePushToken:YES]; + [pushSystemHelper registerForRemoteNotificationsTypes:[self notificationType] + providesNotificationSettings:self.supportsAppNotificationSettings + completionHandler:completionHandler]; } -- (void)requestProvisionalNotificationAuthorization { +- (void)requestProvisionalNotificationAuthorizationWithCompletionHandler:(void (^)(BOOL granted, + NSError *error))completionHandler { id pushSystemHelper = [BAInjection injectProtocol:@protocol(BAPushSystemHelperProtocol)]; [pushSystemHelper registerForProvisionalNotifications:[self notificationType] - providesNotificationSettings:self.supportsAppNotificationSettings]; + providesNotificationSettings:self.supportsAppNotificationSettings + completionHandler:completionHandler]; } - (void)openSystemNotificationSettings { @@ -309,12 +290,6 @@ - (void)refreshToken { [[UIApplication sharedApplication] registerForRemoteNotifications]; } -+ (void)setNotificationsCategories:(NSSet *)categories { - id pushSystemHelper = - [BAInjection injectProtocol:@protocol(BAPushSystemHelperProtocol)]; - [pushSystemHelper registerCategories:categories]; -} - - (void)registerToken:(NSData *)token { NSString *stringToken = [BAStringUtils hexStringValueForData:token]; @@ -477,25 +452,6 @@ - (void)internalHandleNotification:(NSDictionary *)userInfo actionIdentifier:(NS [BATrackerCenter trackPrivateEvent:@"_PUSH_ACTION" parameters:eventParameters]; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (void)internalHandleRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - BANotificationAuthorization *notifAuth = [BACoreCenter instance].status.notificationAuthorization; - BOOL alertEnabled = (notificationSettings.types | UIUserNotificationTypeAlert) > 0; - if (alertEnabled && - [BANotificationAuthorization applicationSettings] == BatchPushNotificationSettingStatusUndefined) { - [notifAuth setApplicationSettings:BatchPushNotificationSettingStatusEnabled skipServerEvent:true]; - } - [notifAuth settingsMayHaveChanged]; - - if (![[BAPushCenter instance] shouldAutomaticallyRetreivePushToken]) { - return; - } - - [[UIApplication sharedApplication] registerForRemoteNotifications]; -} -#pragma clang diagnostic pop - #pragma mark - #pragma mark Manual integration methods @@ -555,43 +511,6 @@ - (BOOL)isBatchPush:(NSDictionary *)userInfo { return TRUE; } -- (void)handleNotification:(NSDictionary *)userInfo { - if ([self passesManualIntegrationPreflightChecks]) { - if (![userInfo isKindOfClass:[NSDictionary class]] || [userInfo count] == 0) { - [BALogger publicForDomain:@"Push" message:@"Cannot process a push payload that's null or empty. Ignoring."]; - return; - } - - [self parseNotification:userInfo fetchCompletionHandler:nil originatesFromUNDelegateResponse:NO]; - } -} - -- (void)handleNotification:(NSDictionary *)userInfo actionIdentifier:(NSString *)identifier { - if ([self passesManualIntegrationPreflightChecks]) { - if (![userInfo isKindOfClass:[NSDictionary class]] || [userInfo count] == 0) { - [BALogger publicForDomain:@"Push" message:@"Cannot process a push payload that's null or empty. Ignoring."]; - return; - } - - if (![identifier isKindOfClass:[NSString class]] || [identifier length] == 0) { - [BALogger publicForDomain:@"Push" - message:@"Cannot process a push action identifier that's null or empty. Ignoring."]; - return; - } - - [self internalHandleNotification:userInfo actionIdentifier:identifier]; - } -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (void)handleRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - if ([self passesManualIntegrationPreflightChecks]) { - [self internalHandleRegisterUserNotificationSettings:notificationSettings]; - } -} -#pragma clang diagnostic pop - #pragma mark - #pragma mark UNUserNotificationCenterDelegate @@ -656,54 +575,6 @@ - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotif error != nil ? error.localizedDescription : @"Unknown error"]; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo -#pragma clang diagnostic pop -{ - [self parseNotification:userInfo fetchCompletionHandler:nil originatesFromUNDelegateResponse:NO]; -} - -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [self parseNotification:userInfo fetchCompletionHandler:completionHandler originatesFromUNDelegateResponse:NO]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (void)application:(UIApplication *)application - didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings -#pragma clang diagnostic pop -{ - [self internalHandleRegisterUserNotificationSettings:notificationSettings]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forRemoteNotification:(NSDictionary *)userInfo - completionHandler:(void (^)(void))completionHandler -#pragma clang diagnostic pop -{ - [self internalHandleNotification:userInfo actionIdentifier:identifier]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forRemoteNotification:(NSDictionary *)userInfo - withResponseInfo:(NSDictionary *)responseInfo - completionHandler:(void (^)(void))completionHandler -#pragma clang diagnostic pop -{ - [self internalHandleNotification:userInfo actionIdentifier:identifier]; -} - #pragma clang diagnostic pop @end diff --git a/Sources/Batch/Modules/Push/BAPushSystemHelper.m b/Sources/Batch/Modules/Push/BAPushSystemHelper.m index cf01402..f47d1b2 100644 --- a/Sources/Batch/Modules/Push/BAPushSystemHelper.m +++ b/Sources/Batch/Modules/Push/BAPushSystemHelper.m @@ -18,18 +18,20 @@ @implementation BAPushSystemHelper - (void)registerForRemoteNotificationsTypes:(BatchNotificationType)notifType - providesNotificationSettings:(BOOL)providesSettings { + providesNotificationSettings:(BOOL)providesSettings + completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler { UNAuthorizationOptions options = [self systemOptionsForBatchTypes:notifType]; if (providesSettings) { options |= UNAuthorizationOptionProvidesAppNotificationSettings; } - [self requestAuthorization:options]; + [self requestAuthorization:options completionHandler:completionHandler]; } - (void)registerForProvisionalNotifications:(BatchNotificationType)notifType - providesNotificationSettings:(BOOL)providesSettings { + providesNotificationSettings:(BOOL)providesSettings + completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler { UNAuthorizationOptions options = [self systemOptionsForBatchTypes:notifType]; options |= UNAuthorizationOptionProvisional; @@ -38,39 +40,7 @@ - (void)registerForProvisionalNotifications:(BatchNotificationType)notifType options |= UNAuthorizationOptionProvidesAppNotificationSettings; } - [self requestAuthorization:options]; -} - -- (void)registerCategories:(NSSet *)categories { - NSSet *categoriesToRegister = nil; - - if ([categories count] > 0) { - if ([self set:categories onlyContainsElementsOfClass:[UNNotificationCategory class]]) { - categoriesToRegister = categories; - } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - - else if ([self set:categories onlyContainsElementsOfClass:[UIUserNotificationCategory class]]) { - [BALogger debugForDomain:@"BAPushCompatUN" - message:@"Converting instances of UIUserNotificationCategory to UNNotificationCategory"]; - categoriesToRegister = [self convertLegacyCategoriesForSet:categories]; - } -#pragma clang diagnostic pop - - else { - [BALogger publicForDomain:@"BatchPush" - message:@"Provided categories set contains more than one kind of class, Batch will NOT " - @"register ANY actions, please only fill the set with UNNotificationCategory or " - @"UIUserNotificationCategory instances."]; - } - } - - if (categoriesToRegister == nil) { - categoriesToRegister = [NSSet new]; - } - - [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categoriesToRegister]; + [self requestAuthorization:options completionHandler:completionHandler]; } #pragma mark Private methods @@ -97,153 +67,28 @@ - (UNAuthorizationOptions)systemOptionsForBatchTypes:(BatchNotificationType)batc return retOptions; } -- (void)requestAuthorization:(UNAuthorizationOptions)options { - BatchPushNotificationSettingStatus currentStatus = [BANotificationAuthorization applicationSettings]; - +- (void)requestAuthorization:(UNAuthorizationOptions)options + completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler { [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *_Nullable error) { [BAThreading performBlockOnMainThreadAsync:^{ [[UIApplication sharedApplication] registerForRemoteNotifications]; - if (currentStatus == BatchPushNotificationSettingStatusUndefined) { - [[NSNotificationCenter defaultCenter] - postNotificationName:BatchPushUserDidAnswerAuthorizationRequestNotification - object:nil - userInfo:@{ - BatchPushUserDidAcceptKey : [NSNumber numberWithBool:granted] - }]; - } + [[NSNotificationCenter defaultCenter] + postNotificationName:BatchPushUserDidAnswerAuthorizationRequestNotification + object:nil + userInfo:@{BatchPushUserDidAcceptKey : [NSNumber numberWithBool:granted]}]; }]; BANotificationAuthorization *notifAuth = [BACoreCenter instance].status.notificationAuthorization; - if (granted && [BANotificationAuthorization applicationSettings] == - BatchPushNotificationSettingStatusUndefined) { - [notifAuth setApplicationSettings:BatchPushNotificationSettingStatusEnabled - skipServerEvent:true]; - } [notifAuth settingsMayHaveChanged]; - }]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (NSSet *)convertLegacyCategoriesForSet: - (NSSet *)legacyCategories { - NSMutableSet *retVal = nil; - - if (legacyCategories == nil) { - return nil; - } - - retVal = [NSMutableSet new]; - - BOOL alreadyWarnedAboutContext = false; - - for (UIUserNotificationCategory *legacyCategory in legacyCategories) { - NSArray *actions = - [self convertLegacyActions:[legacyCategory actionsForContext:UIUserNotificationActionContextDefault]]; - NSArray *minimalActions = - [self convertLegacyActions:[legacyCategory actionsForContext:UIUserNotificationActionContextMinimal]]; - - if (!alreadyWarnedAboutContext && [actions count] > 0 && [minimalActions count] > 0) { - alreadyWarnedAboutContext = true; - [BALogger - publicForDomain:@"BatchPush" - message:@"You're attempting to register legacy UIUserNotificationCategory instances rather " - @"than UNNotificationCategory instances on iOS 10. Batch usually silently converts " - @"them, but since you've added both actions for Default and Minimal contexts, Batch " - @"will only register the Default context actions to iOS, since iOS 10 does not support " - @"this deprecated behaviour. Only the first two actions will be displayed in a minimal " - @"context. In order to fix this warning, please register native UNNotificationCategory " - @"instances when, and only when, running on iOS 10 or higher."]; - } - - // If we don't have actions, use the minimal actions - if ([actions count] == 0) { - actions = minimalActions; - } - - if (actions == nil) { - actions = @[]; - } - - // If users want native iOS 10 functions, they should register it themselves - [retVal addObject:[UNNotificationCategory categoryWithIdentifier:legacyCategory.identifier - actions:actions - intentIdentifiers:@[] - options:0]]; - } - - return retVal; -} - -- (NSArray *)convertLegacyActions:(NSArray *)legacyActions { - NSMutableArray *retVal = nil; - - if (legacyActions == nil) { - return nil; - } - - retVal = [NSMutableArray new]; - - for (UIUserNotificationAction *legacyAction in legacyActions) { - [retVal addObject:[self convertLegacyAction:legacyAction]]; - } - - return retVal; -} - -- (nonnull UNNotificationAction *)convertLegacyAction:(nonnull UIUserNotificationAction *)legacyAction { - UNNotificationActionOptions options = UNNotificationActionOptionNone; - - if (legacyAction.destructive) { - options |= UNNotificationActionOptionDestructive; - } - - if (legacyAction.activationMode == UIUserNotificationActivationModeForeground) { - options |= UNNotificationActionOptionForeground; - } - - if (legacyAction.isAuthenticationRequired) { - options |= UNNotificationActionOptionAuthenticationRequired; - } - - if (legacyAction.behavior == UIUserNotificationActionBehaviorTextInput) { - NSString *buttonTitle = - [[legacyAction parameters] objectForKey:UIUserNotificationTextInputActionButtonTitleKey]; - if (buttonTitle == nil) { - buttonTitle = @"Send"; // That's possibly a bad idea. - } - - NSString *placeholder = [[legacyAction parameters] objectForKey:BatchUserActionInputTextFieldPlaceholderKey]; - if (placeholder == nil) { - placeholder = @""; - } - - return [UNTextInputNotificationAction actionWithIdentifier:legacyAction.identifier - title:legacyAction.title - options:options - textInputButtonTitle:buttonTitle - textInputPlaceholder:placeholder]; - } else { - return [UNNotificationAction actionWithIdentifier:legacyAction.identifier - title:legacyAction.title - options:options]; - } -} - -#pragma clang diagnostic pop -- (BOOL)set:(NSSet *)set onlyContainsElementsOfClass:(Class)class { - for (NSObject *item in set) { - if (![item isKindOfClass:class]) { - return false; - } - } - return true; + if (completionHandler) { + completionHandler(granted, error); + } + }]; } @end diff --git a/Sources/Batch/Modules/Push/BAPushSystemHelperProtocol.h b/Sources/Batch/Modules/Push/BAPushSystemHelperProtocol.h index 80331d3..0c70745 100644 --- a/Sources/Batch/Modules/Push/BAPushSystemHelperProtocol.h +++ b/Sources/Batch/Modules/Push/BAPushSystemHelperProtocol.h @@ -13,15 +13,13 @@ // Register for remote notifications for the given types and whether the app should declare that it supports a settings // deeplink or not - (void)registerForRemoteNotificationsTypes:(BatchNotificationType)notifType - providesNotificationSettings:(BOOL)providesSettings; + providesNotificationSettings:(BOOL)providesSettings + completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler; // Register for provisional notifications for the given types and whether the app should declare that it supports a // settings deeplink or not - (void)registerForProvisionalNotifications:(BatchNotificationType)notifType - providesNotificationSettings:(BOOL)providesSettings; - -// Register the given categories to iOS. The set is not typed so the user can give us either iOS 8+ or 10+ classes (or -// any class, you must check the input beforehand) -- (void)registerCategories:(NSSet *)categories; + providesNotificationSettings:(BOOL)providesSettings + completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler; @end diff --git a/Sources/Batch/Modules/Tracker/BAEvent.m b/Sources/Batch/Modules/Tracker/BAEvent.m index 1c4380f..a2ab8c3 100644 --- a/Sources/Batch/Modules/Tracker/BAEvent.m +++ b/Sources/Batch/Modules/Tracker/BAEvent.m @@ -184,6 +184,11 @@ + (NSArray *)identifiersOfEvents:(NSArray *)events { return [NSArray arrayWithArray:eventIDs]; } +- (NSString *)debugDescription { + return [NSString stringWithFormat:@"BAEvent(name: '%@', parameters count: %lu)", self.name, + (unsigned long)self.parametersDictionary.count]; +} + @end @implementation BACollapsableEvent diff --git a/Sources/Batch/Modules/Tracker/BAEventSQLiteDatasource.m b/Sources/Batch/Modules/Tracker/BAEventSQLiteDatasource.m index 139248b..4b97383 100644 --- a/Sources/Batch/Modules/Tracker/BAEventSQLiteDatasource.m +++ b/Sources/Batch/Modules/Tracker/BAEventSQLiteDatasource.m @@ -266,12 +266,9 @@ - (NSArray *)eventsToSend:(NSUInteger)count { [insertString appendString:[parameterNames objectAtIndex:i]]; } - // LIKE should be read as LIKE "\_%" - NSString *selectSQL = - [NSString stringWithFormat:@"SELECT %@, %@ FROM %@ WHERE %@ IN (%d,%d) ORDER BY CASE WHEN %@ LIKE " - @"\"\\_%%\" ESCAPE '\\' THEN 1 ELSE 0 END DESC, %@ DESC", - COLUMN_DB_ID, insertString, TABLE_EVENTS, COLUMN_STATE, BAEventStateNew, - BAEventStateOld, COLUMN_NAME, COLUMN_DB_ID]; + NSString *selectSQL = [NSString stringWithFormat:@"SELECT %@, %@ FROM %@ WHERE %@ IN (%d,%d) ORDER BY %@ ASC", + COLUMN_DB_ID, insertString, TABLE_EVENTS, COLUMN_STATE, + BAEventStateNew, BAEventStateOld, COLUMN_DB_ID]; if (count > 0) { selectSQL = [NSString stringWithFormat:@"%@ LIMIT %lu", selectSQL, (unsigned long)count]; } diff --git a/Sources/Batch/Modules/Tracker/BATEventTrackerProtocol.swift b/Sources/Batch/Modules/Tracker/BATEventTrackerProtocol.swift new file mode 100644 index 0000000..d11d4eb --- /dev/null +++ b/Sources/Batch/Modules/Tracker/BATEventTrackerProtocol.swift @@ -0,0 +1,31 @@ +// +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Foundation + +// Protocol that defines Batch's event tracker to facilitate dependency injection +// For now, this only handles private events +@objc(BATEventTrackerProtocol) +public protocol BATEventTracker { + // Tracks a public event. Do not add "E." to the event name, the tracker will do it. + @objc(trackPublicEvent:attributes:) + func trackPublicEvent(name: String, attributes: BatchEventAttributes?) + + @objc(trackPrivateEvent:parameters:collapsable:) + func trackPrivateEvent(name: String, parameters: [AnyHashable: Any]?, collapsable: Bool) + + @objc(trackManualPrivateEvent:) + func trackManualPrivateEvent(_ event: BAEvent) + + @objc(trackLocation:) + func trackLocation(_ location: CLLocation) +} + +extension BATEventTracker { + func trackPrivateEvent(event: BATInternalEvent, parameters: [AnyHashable: Any]?, collapsable: Bool) { + trackPrivateEvent(name: event.rawValue, parameters: parameters, collapsable: collapsable) + } +} diff --git a/Sources/Batch/Modules/Tracker/BATrackerCenter.h b/Sources/Batch/Modules/Tracker/BATrackerCenter.h index a0884a2..c3faeb3 100644 --- a/Sources/Batch/Modules/Tracker/BATrackerCenter.h +++ b/Sources/Batch/Modules/Tracker/BATrackerCenter.h @@ -20,31 +20,6 @@ @class BatchEventData; -/*! - @enum BATrackerMode - @abstract Possible causes of webservice failure. - */ -enum { - /*! - Value to completly switch off the event tracker. - */ - BATrackerModeOFF = 0, - - /*! - Value to stop sending the event (event will continue to be registred in DB). - */ - BATrackerModeDB_ONLY = 1, - - /*! - Value to completely activate the event tracker (default value). - */ - BATrackerModeON = 2 -}; -/*! - @typedef BATrackerMode - */ -typedef NSInteger BATrackerMode; - /*! @class BATrackerCenter @abstract Central control point of Batch event tracking services. @@ -59,13 +34,6 @@ typedef NSInteger BATrackerMode; */ + (BATrackerCenter *_Nonnull)instance __attribute__((warn_unused_result)); -/*! - @method currentMode - @abstract Helper to retieve the sending mode. - @return The current BATrackerMode stored or default. - */ -+ (BATrackerMode)currentMode __attribute__((warn_unused_result)); - /*! @method batchWillStart @abstract Called before Batch runtime begins its process. @@ -73,13 +41,6 @@ typedef NSInteger BATrackerMode; */ + (void)batchWillStart; -/*! - Track an uncollapsable publicevent with label and data - - Do not prepend "E." to the event name: this method will do it - */ -+ (void)trackPublicEvent:(nonnull NSString *)name label:(nullable NSString *)label data:(nullable BatchEventData *)data; - /*! Track an uncollapsable private event with parameters */ @@ -98,12 +59,6 @@ typedef NSInteger BATrackerMode; */ + (void)trackManualPrivateEvent:(nonnull BAEvent *)event; -/*! - @method trackLocation: - @abstract Track an location event - */ -+ (void)trackLocation:(nonnull CLLocation *)location; - /*! @method datasource @abstract Return the backing datasource diff --git a/Sources/Batch/Modules/Tracker/BATrackerCenter.m b/Sources/Batch/Modules/Tracker/BATrackerCenter.m index c162608..7f5a98b 100644 --- a/Sources/Batch/Modules/Tracker/BATrackerCenter.m +++ b/Sources/Batch/Modules/Tracker/BATrackerCenter.m @@ -19,7 +19,8 @@ #import #import #import -#import +#import +#import #import @@ -42,6 +43,112 @@ @interface BATrackerCenter () { @property (atomic) NSString *flushHash; +- (BOOL)internalTrackEvent:(NSString *)name withParameters:(NSDictionary *)parameters collapsable:(BOOL)collapsable; + +- (BOOL)internalTrackEvent:(BAEvent *)event; + +@end + +#pragma mark - +#pragma mark BATEventTrackerProtocol conformance +@interface BATrackerCenter (BATEventTrackerProtocolConformance) +@end + +@implementation BATrackerCenter (BATEventTrackerProtocolConformance) + +- (void)trackPublicEvent:(nonnull NSString *)name attributes:(nullable BatchEventAttributes *)attributes { + name = [@"E." stringByAppendingString:name.uppercaseString]; + + NSDictionary *parameters; + if (attributes) { + NSError *error = nil; + parameters = [BATEventAttributesSerializer serializeWithEventAttributes:attributes error:&error]; + if (parameters == nil) { + [BALogger publicForDomain:@"Profile" + message:@"Failed to track event: internal error when serializing data"]; + [BALogger errorForDomain:DEBUG_DOMAIN message:@"Failed to track event: %@", error.debugDescription]; + return; + } + } else { + parameters = [NSDictionary dictionary]; + } + + if ([self internalTrackEvent:name withParameters:parameters collapsable:false]) { + [[BALocalCampaignsCenter instance] processTrackerPublicEventNamed:name + label:attributes._label + attributes:attributes]; + } +} + +- (void)trackLocation:(nonnull CLLocation *)location { + if (location == nil) { + return; + } + + NSDate *currentDate = [NSDate date]; + + // See if a location update should be sent. + BOOL shouldTrackLocation = NO; + + if (_lastTrackedLocationTimestamp == nil) { + [BALogger debugForDomain:DEBUG_DOMAIN + message:@"Tracking location because no previous location has been tracked"]; + shouldTrackLocation = YES; + } else if ([currentDate timeIntervalSinceDate:_lastTrackedLocationTimestamp] * 1000 >= + LOCATION_UPDATE_MINIMUM_TIME_MS) { + [BALogger + debugForDomain:DEBUG_DOMAIN + message: + @"Tracking location because the minimum time interval since the last sent update has passed"]; + shouldTrackLocation = YES; + } + + // Yes this could have been a big "if", but it would have been less readable + if (!shouldTrackLocation) { + [BALogger debugForDomain:DEBUG_DOMAIN message:@"Ignoring location track"]; + return; + } + + // According to the doc, if horizontalAccuracy is negative, it's an invalid location + // Don't bother sending it + if (location.horizontalAccuracy < 0) { + return; + } + + NSDate *systemTs = location.timestamp; + id timestamp; + if (systemTs == nil) { + timestamp = [NSNull null]; + } else { + timestamp = [NSNumber numberWithDouble:floor([systemTs timeIntervalSince1970] * 1000)]; + } + + NSDictionary *params = @{ + @"lat" : [NSNumber numberWithDouble:location.coordinate.latitude], + @"lng" : [NSNumber numberWithDouble:location.coordinate.longitude], + @"acc" : [NSNumber numberWithDouble:location.horizontalAccuracy], + @"date" : timestamp + }; + + [self trackPrivateEvent:@"_LOCATION_CHANGED" parameters:params collapsable:YES]; + + _lastTrackedLocationTimestamp = currentDate; +} + +- (void)trackPrivateEvent:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters + collapsable:(BOOL)collapsable { + if ([self internalTrackEvent:name withParameters:parameters collapsable:collapsable]) { + [[BALocalCampaignsCenter instance] processTrackerPrivateEventNamed:name]; + } +} + +- (void)trackManualPrivateEvent:(nonnull BAEvent *)event { + if ([self internalTrackEvent:event]) { + [[BALocalCampaignsCenter instance] processTrackerPrivateEventNamed:event.name]; + } +} + @end @implementation BATrackerCenter @@ -50,10 +157,6 @@ @implementation BATrackerCenter #pragma mark Public methods + (void)batchWillStart { - if ([BATrackerCenter currentMode] == BATrackerModeOFF) { - return; - } - // This prevent the tracker to be subscribe many times to the events in case the developper call `startWithAPIKey:` // many times. if (![[BACoreCenter instance].status isRunning]) { @@ -61,12 +164,6 @@ + (void)batchWillStart { } } -+ (void)trackPublicEvent:(nonnull NSString *)name - label:(nullable NSString *)label - data:(nullable BatchEventData *)data { - [[BATrackerCenter instance] trackPublicEvent:name label:label data:data]; -} - + (void)trackPrivateEvent:(nonnull NSString *)name parameters:(nullable NSDictionary *)parameters { [[BATrackerCenter instance] trackPrivateEvent:name parameters:parameters collapsable:NO]; } @@ -81,10 +178,6 @@ + (void)trackManualPrivateEvent:(nonnull BAEvent *)event { [[BATrackerCenter instance] trackManualPrivateEvent:event]; } -+ (void)trackLocation:(nonnull CLLocation *)location { - [[BATrackerCenter instance] trackLocation:location]; -} - + (id)datasource { return [[BATrackerCenter instance] datasource]; } @@ -103,18 +196,6 @@ + (BATrackerCenter *)instance { return sharedInstance; } -+ (BATrackerMode)currentMode { - NSNumber *mode = [BAParameter objectForKey:kParametersTrackerStateKey fallback:kParametersTrackerStateValue]; - - if ([mode isEqualToNumber:@0]) { - return BATrackerModeOFF; - } else if ([mode isEqualToNumber:@1]) { - return BATrackerModeDB_ONLY; - } else { - return BATrackerModeON; - } -} - #pragma mark - #pragma mark Instance methods @@ -189,98 +270,6 @@ - (BATrackerScheduler *)scheduler { return _scheduler; } -- (void)trackPublicEvent:(nonnull NSString *)name - label:(nullable NSString *)label - data:(nullable BatchEventData *)data { - name = [@"E." stringByAppendingString:name]; - - NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:4]; - if ([label isKindOfClass:[NSString class]]) { - if (label.length > 200) { - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Event label is too long, removing it."]; - } else { - parameters[BA_PUBLIC_EVENT_KEY_LABEL] = label; - } - } - - if ([data isKindOfClass:[BatchEventData class]]) { - [parameters addEntriesFromDictionary:[data _internalDictionaryRepresentation]]; - } - - if ([self internalTrackEvent:name withParameters:parameters collapsable:false]) { - [[BALocalCampaignsCenter instance] processTrackerPublicEventNamed:name label:label data:data]; - } -} - -- (void)trackPrivateEvent:(nonnull NSString *)name - parameters:(nullable NSDictionary *)parameters - collapsable:(BOOL)collapsable { - if ([self internalTrackEvent:name withParameters:parameters collapsable:collapsable]) { - [[BALocalCampaignsCenter instance] processTrackerPrivateEventNamed:name]; - } -} - -- (void)trackManualPrivateEvent:(nonnull BAEvent *)event { - if ([self internalTrackEvent:event]) { - [[BALocalCampaignsCenter instance] processTrackerPrivateEventNamed:event.name]; - } -} - -- (void)trackLocation:(nonnull CLLocation *)location { - if (location == nil) { - return; - } - - NSDate *currentDate = [NSDate date]; - - // See if a location update should be sent. - BOOL shouldTrackLocation = NO; - - if (_lastTrackedLocationTimestamp == nil) { - [BALogger debugForDomain:DEBUG_DOMAIN - message:@"Tracking location because no previous location has been tracked"]; - shouldTrackLocation = YES; - } else if ([currentDate timeIntervalSinceDate:_lastTrackedLocationTimestamp] * 1000 >= - LOCATION_UPDATE_MINIMUM_TIME_MS) { - [BALogger - debugForDomain:DEBUG_DOMAIN - message: - @"Tracking location because the minimum time interval since the last sent update has passed"]; - shouldTrackLocation = YES; - } - - // Yes this could have been a big "if", but it would have been less readable - if (!shouldTrackLocation) { - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Ignoring location track"]; - return; - } - - // According to the doc, if horizontalAccuracy is negative, it's an invalid location - // Don't bother sending it - if (location.horizontalAccuracy < 0) { - return; - } - - NSDate *systemTs = location.timestamp; - id timestamp; - if (systemTs == nil) { - timestamp = [NSNull null]; - } else { - timestamp = [NSNumber numberWithDouble:floor([systemTs timeIntervalSince1970] * 1000)]; - } - - NSDictionary *params = @{ - @"lat" : [NSNumber numberWithDouble:location.coordinate.latitude], - @"lng" : [NSNumber numberWithDouble:location.coordinate.longitude], - @"acc" : [NSNumber numberWithDouble:location.horizontalAccuracy], - @"date" : timestamp - }; - - [self trackPrivateEvent:@"_LOCATION_CHANGED" parameters:params collapsable:YES]; - - _lastTrackedLocationTimestamp = currentDate; -} - /** Underlying method that should never be called directly @@ -309,10 +298,6 @@ - (BOOL)internalTrackEvent:(BAEvent *)event { [_signpostHelper trackEvent:name withParameters:parameters collapsable:collapsable]; - if ([BATrackerCenter currentMode] == BATrackerModeOFF) { - return false; - } - if ([_optOutModule isOptedOut]) { [BALogger errorForDomain:@"Batch.User" message:@"Batch is opted out from: refusing to track event"]; } diff --git a/Sources/Batch/Modules/Tracker/BATrackerSender.m b/Sources/Batch/Modules/Tracker/BATrackerSender.m index eca5f0c..5bd0868 100644 --- a/Sources/Batch/Modules/Tracker/BATrackerSender.m +++ b/Sources/Batch/Modules/Tracker/BATrackerSender.m @@ -36,11 +36,6 @@ - (instancetype)init { } - (BOOL)send { - if ([BATrackerCenter currentMode] != BATrackerModeON) { - // Nothing to do, not send. - return NO; - } - if (_isSending) { return YES; } diff --git a/Sources/Batch/Modules/User/BAUserDataEditor.h b/Sources/Batch/Modules/User/BAInstallDataEditor.h similarity index 80% rename from Sources/Batch/Modules/User/BAUserDataEditor.h rename to Sources/Batch/Modules/User/BAInstallDataEditor.h index 186539b..a589358 100644 --- a/Sources/Batch/Modules/User/BAUserDataEditor.h +++ b/Sources/Batch/Modules/User/BAInstallDataEditor.h @@ -1,5 +1,5 @@ // -// BAUserDataEditor.h +// BAInstallDataEditor.h // Batch // // https://batch.com @@ -9,21 +9,29 @@ #import #import -@interface BAUserDataEditor : NSObject +/// Error domain of error when ``BatchInstallDataEditor`` fails. +FOUNDATION_EXPORT NSErrorDomain const _Nonnull BAInstallDataEditorErrorDomain; -- (void)setLanguage:(nullable NSString *)language; +/// User data editor error codes. +typedef NS_ERROR_ENUM(BAInstallDataEditorErrorDomain, BAInstallDataEditorError){ -- (void)setRegion:(nullable NSString *)region; + /// Internal error. + BAInstallDataEditorErrorInternal = 0, -- (void)setIdentifier:(nullable NSString *)identifier; + /// The key is invalid. This also applies to tag collection names, as they're considered keys. + BAInstallDataEditorErrorInvalidKey = 1, -- (void)setAttributionIdentifier:(nullable NSString *)attributionID; + /// The value is invalid: see the error description for more info. + BAInstallDataEditorErrorInvalidValue = 2, +}; -- (BOOL)setEmail:(nullable NSString *)email error:(NSError *_Nullable *_Nullable)error; +@interface BAInstallDataEditor : NSObject -- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state; +- (void)setLanguage:(nullable NSString *)language; -- (void)setAttribute:(nullable NSObject *)attribute forKey:(nonnull NSString *)key; +- (void)setRegion:(nullable NSString *)region; + +- (void)setIdentifier:(nullable NSString *)identifier; - (BOOL)setBooleanAttribute:(BOOL)attribute forKey:(nonnull NSString *)key diff --git a/Sources/Batch/Modules/User/BAUserDataEditor.m b/Sources/Batch/Modules/User/BAInstallDataEditor.m similarity index 74% rename from Sources/Batch/Modules/User/BAUserDataEditor.m rename to Sources/Batch/Modules/User/BAInstallDataEditor.m index da0b28e..e44b2b9 100644 --- a/Sources/Batch/Modules/User/BAUserDataEditor.m +++ b/Sources/Batch/Modules/User/BAInstallDataEditor.m @@ -7,18 +7,17 @@ // #import -#import #import +#import #import #import #import #import -#import #import #import -#import #import #import +#import #define PUBLIC_DOMAIN @"BatchUser - Editor" #define DEBUG_DOMAIN @"UserDataEditor" @@ -44,34 +43,33 @@ #define ENSURE_ATTRIBUTE_VALUE_CLASS(attrValue, expectedClass) \ if (attrValue == nil) { \ - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue \ + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue \ reason:@"The attribute's value cannot be nil. Did you mean to use " \ @"'removeAttributeForKey'?"]; \ return false; \ } \ if (![attrValue isKindOfClass:expectedClass]) { \ - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue \ + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue \ reason:@"The attribute's value isn't of the expected class (%@)", \ NSStringFromClass(expectedClass)]; \ return false; \ } -@interface BAUserDataEditor () +NSErrorDomain const BAInstallDataEditorErrorDomain = @"com.batch.ios.installdataeditor"; + +@interface BAInstallDataEditor () @property (readwrite, atomic) volatile BOOL wasApplied; @end -@implementation BAUserDataEditor { +@implementation BAInstallDataEditor { NSMutableArray *_operationQueue; id _datasource; NSRegularExpression *_attributeNameValidationRegexp; BOOL _updatedFields[3]; NSString *_userFields[3]; - - /// Email subscription - BAUserEmailSubscription *_emailSubscription; } - (instancetype)init { @@ -144,120 +142,6 @@ - (void)setIdentifier:(nullable NSString *)identifier { _userFields[IDENTIFIER_INDEX] = identifier; } -- (void)setAttributionIdentifier:(nullable NSString *)attributionID { - if (![BANullHelper isNull:attributionID] && [attributionID isKindOfClass:[NSString class]] && - [[NSUUID alloc] initWithUUIDString:attributionID] == nil) { - [BALogger - publicForDomain:PUBLIC_DOMAIN - message: - @"setAttributionIdentifier called with invalid attribution identifier (must be a valid UUID)"]; - return; - } - - [self addfirstToQueueSynchronized:^BOOL { - BAUserProfile *userProfile = [BAUserProfile defaultUserProfile]; - NSString *oldAttributionID = userProfile.attributionID; - userProfile.attributionID = attributionID; - if (![oldAttributionID isEqualToString:attributionID]) { - [userProfile sendAttributionIDChangedEvent]; - } - return YES; - }]; -} - -- (BOOL)setEmail:(nullable NSString *)email error:(NSError **)error { - INIT_AND_BLANK_ERROR_IF_NEEDED(error) - - // Ensure we already have a custom user identifier - // or setIdentifier has been previously called in this editor instance - if ([BatchUser identifier] == nil && - (_updatedFields[IDENTIFIER_INDEX] == NO || - (_updatedFields[IDENTIFIER_INDEX] == YES && _userFields[IDENTIFIER_INDEX] == nil))) { - *error = [self - logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInternal - reason:@"setEmail called even though no custom user id is set. Please ensure to call " - @"setIdentifier before using this method."]; - return false; - } - - // Deleting case - if (email == nil) { - if (_emailSubscription == nil) { - _emailSubscription = [[BAUserEmailSubscription alloc] initWithEmail:nil]; - } else { - [_emailSubscription setEmail:nil]; - } - return true; - } - - // Ensure email is not too long - if ([BAEmailUtils isEmailTooLong:email]) { - *error = - [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue - reason:@"Email can't be longer than %d characters. Ignoring.", EMAIL_MAX_LENGTH]; - return false; - } - - // Ensure email is valid - if (![BAEmailUtils isValidEmail:email]) { - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue - reason:@"setEmail called with invalid email format. Please ensure to " - @"respect the following regex: .@.\\..* "]; - return false; - } - - if (_emailSubscription == nil) { - _emailSubscription = [[BAUserEmailSubscription alloc] - initWithEmail:[email stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; - } else { - [_emailSubscription setEmail:[email stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; - } - - return true; -} - -- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state { - if (_emailSubscription == nil) { - _emailSubscription = [[BAUserEmailSubscription alloc] init]; - } - [_emailSubscription setEmailSubscriptionState:state forKind:BAEmailKindMarketing]; -} - -- (void)setAttribute:(nullable NSObject *)attribute forKey:(nonnull NSString *)key { - if (attribute == nil) { - [self removeAttributeForKey:key]; - return; - } - - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Setting attribute (legacy) '%@' for key '%@'", attribute, key]; - - // Let's guess the object type - // Quick reminder of supported objects: - // - NSNumber - // - NSString - // - NSDate - // - NSURL - if ([attribute isKindOfClass:[NSString class]]) { - [self setStringAttribute:(NSString *)attribute forKey:key error:nil]; - return; - } else if ([attribute isKindOfClass:[NSDate class]]) { - [self setDateAttribute:(NSDate *)attribute forKey:key error:nil]; - return; - } else if ([attribute isKindOfClass:[NSNumber class]]) { - [self setNumberAttribute:(NSNumber *)attribute forKey:key error:nil]; - return; - } else if ([attribute isKindOfClass:[NSURL class]]) { - [self setURLAttribute:(NSURL *)attribute forKey:key error:nil]; - return; - } - - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Invalid attribute value. Please check the documentation for accepted values. Ignoring " - @"attribute '%@'.", - key]; - return; -} - - (BOOL)setBooleanAttribute:(BOOL)attribute forKey:(nonnull NSString *)key error:(NSError **)error { INIT_AND_BLANK_ERROR_IF_NEEDED(error) VALIDATE_ATTRIBUTE_KEY_OR_BAIL() @@ -292,7 +176,7 @@ - (BOOL)setStringAttribute:(nonnull NSString *)attribute if ([((NSString *)attribute) length] > ATTR_STRING_MAX_LENGTH) { *error = [self - logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue + logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue reason: @"String attributes can't be longer than %d characters. Ignoring attribute '%@'.", ATTR_STRING_MAX_LENGTH, key]; @@ -316,7 +200,7 @@ - (BOOL)setURLAttribute:(nonnull NSURL *)attribute if ([(attribute.absoluteString) length] > ATTR_URL_MAX_LENGTH) { *error = [self - logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue + logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue reason:@"URL attributes can't be longer than %d characters. Ignoring attribute '%@'.", ATTR_URL_MAX_LENGTH, key]; return false; @@ -324,7 +208,7 @@ - (BOOL)setURLAttribute:(nonnull NSURL *)attribute if (attribute.scheme == nil || attribute.host == nil) { *error = [self - logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue + logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue reason:@"URL attributes must respect format " @"'scheme://[authority][path][?query][#fragment]'. Ignoring attribute '%@'.", key]; @@ -412,7 +296,7 @@ - (BOOL)setNumberAttribute:(nonnull NSNumber *)numberAttr forKey:(nonnull NSStri return true; } - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidValue + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidValue reason:@"Unsupported NSNumber type. Ignoring attribute '%@' for value '%@'.", key, numberAttr]; return false; @@ -556,6 +440,11 @@ - (void)clearTagCollection:(nonnull NSString *)collection { @param completion Used mainly for testing purposes. Called when saving operation completed or failed. */ - (void)save:(void (^)(void))completion { + // Execute user update operation outside the editor's queue to avoid debounce + // and have updates available in the ids of the profile event + [self executeUserUpdateOperation]; + + // Add custom data operations to the queue dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(DISPATCH_QUEUE_TIMER * NSEC_PER_MSEC)), [BAUserDataManager sharedQueue], ^{ NSArray *applyQueue; @@ -577,20 +466,7 @@ - (void)save { } - (BOOL)canSave { - if (![[[BACoreCenter instance] status] isRunning]) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Batch must be started before changes to user data can be saved. The changes you've " - @"just tried to save have been discarded."]; - return false; - } - - if ([[BAOptOut instance] isOptedOut]) { - [BALogger publicForDomain:PUBLIC_DOMAIN - message:@"Batch is Opted-Out from: BatchUserDataEditor changes cannot be saved"]; - return false; - } - - return true; + return [BAUserDataManager canSave]; } - (NSArray *)operationQueue { @@ -611,57 +487,31 @@ - (void)addfirstToQueueSynchronized:(BOOL (^)(void))operationBlock { } } -- (BOOL (^)(void))userUpdateOperation { +- (void)executeUserUpdateOperation { if (!_updatedFields[LANGUAGE_INDEX] && !_updatedFields[REGION_INDEX] && !_updatedFields[IDENTIFIER_INDEX]) { // Nothing to do - return nil; + return; } - return ^{ - NSString *previousUserFields[3]; - BAUserProfile *userProfile = [BAUserProfile defaultUserProfile]; - previousUserFields[LANGUAGE_INDEX] = [userProfile language]; - previousUserFields[REGION_INDEX] = [userProfile region]; - previousUserFields[IDENTIFIER_INDEX] = [userProfile customIdentifier]; - - if (self->_updatedFields[LANGUAGE_INDEX]) { - [userProfile setLanguage:self->_userFields[LANGUAGE_INDEX]]; - } - - if (self->_updatedFields[REGION_INDEX]) { - [userProfile setRegion:self->_userFields[REGION_INDEX]]; - } + NSString *previousUserFields[3]; + BAUserProfile *userProfile = [BAUserProfile defaultUserProfile]; + previousUserFields[LANGUAGE_INDEX] = [userProfile language]; + previousUserFields[REGION_INDEX] = [userProfile region]; + previousUserFields[IDENTIFIER_INDEX] = [userProfile customIdentifier]; - if (self->_updatedFields[IDENTIFIER_INDEX]) { - [userProfile setCustomIdentifier:self->_userFields[IDENTIFIER_INDEX]]; - } - - bool updated = false; - for (int i = 0; i < 3; ++i) { - if (previousUserFields[i] != self->_userFields[i] && - ![previousUserFields[i] isEqualToString:self->_userFields[i]]) { - // Field have changed - updated = true; - } - } - - if (updated) { - [userProfile incrementVersion]; - } + if (self->_updatedFields[LANGUAGE_INDEX]) { + [userProfile setLanguage:self->_userFields[LANGUAGE_INDEX]]; + } - [self clearUserFieldsStates]; - return YES; - }; -} + if (self->_updatedFields[REGION_INDEX]) { + [userProfile setRegion:self->_userFields[REGION_INDEX]]; + } -- (BOOL (^)(void))emailUpdateOperation { - if (_emailSubscription == nil) { - return nil; + if (self->_updatedFields[IDENTIFIER_INDEX]) { + [userProfile setCustomIdentifier:self->_userFields[IDENTIFIER_INDEX]]; } - return ^{ - [self->_emailSubscription sendEmailSubscriptionEvent]; - return YES; - }; + + [self clearUserFieldsStates]; } - (void)clearUserFieldsStates { @@ -676,29 +526,18 @@ - (void)clearUserFieldsStates { - (NSArray *)popOperationQueue { NSMutableArray *applyQueue = [_operationQueue mutableCopy]; [_operationQueue removeAllObjects]; - - BOOL (^userUpdateOperation)(void) = [self userUpdateOperation]; - if (userUpdateOperation != nil) { - [applyQueue insertObject:userUpdateOperation atIndex:0]; - } - - BOOL (^emailUpdateOperation)(void) = [self emailUpdateOperation]; - if (emailUpdateOperation != nil) { - [applyQueue addObject:emailUpdateOperation]; - } - return applyQueue; } - (BOOL)validateAttributeKey:(NSString *)key error:(NSError *_Nullable *_Nonnull)error { if (key == nil) { - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidKey + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidKey reason:@"Key cannot be nil. Ignoring attribute '%@'.", key]; return NO; } if (!_attributeNameValidationRegexp) { - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInternal + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInternal reason:@"Internal error. Ignoring attribute '%@'.", key]; return NO; } @@ -712,7 +551,7 @@ - (BOOL)validateAttributeKey:(NSString *)key error:(NSError *_Nullable *_Nonnull } } - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidKey + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidKey reason:@"Invalid key. Please make sure that the key is made of letters, " @"underscores and numbers only (a-zA-Z0-9_). It also can't be longer " @"than 30 characters. Ignoring attribute '%@'.", @@ -729,7 +568,7 @@ - (NSString *)validateAndNormalizeTagCollection:(NSString *)collection va_start(arglist, descriptionFormatString); NSString *operationDescription = [[NSString alloc] initWithFormat:descriptionFormatString arguments:arglist]; va_end(arglist); - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidKey + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidKey reason:@"Collection cannot be nil. Ignoring '%@'.", operationDescription]; return nil; } @@ -739,7 +578,7 @@ - (NSString *)validateAndNormalizeTagCollection:(NSString *)collection va_start(arglist, descriptionFormatString); NSString *operationDescription = [[NSString alloc] initWithFormat:descriptionFormatString arguments:arglist]; va_end(arglist); - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInternal + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInternal reason:@"Internal error. Ignoring %@.", operationDescription]; return nil; } @@ -758,7 +597,7 @@ - (NSString *)validateAndNormalizeTagCollection:(NSString *)collection va_start(arglist, descriptionFormatString); NSString *operationDescription = [[NSString alloc] initWithFormat:descriptionFormatString arguments:arglist]; va_end(arglist); - *error = [self logAndMakeSaveErrorWithCode:BatchUserDataEditorErrorInvalidKey + *error = [self logAndMakeSaveErrorWithCode:BAInstallDataEditorErrorInvalidKey reason:@"Invalid collection. Please make sure that the collection is made of " @"letters, underscores and numbers only (a-zA-Z0-9_). It also can't be " @"longer than 30 characters. Ignoring %@.", @@ -789,13 +628,13 @@ - (NSString *)normalizeTag:(NSString *)tag { return [tag lowercaseString]; } -- (NSError *)logAndMakeSaveErrorWithCode:(BatchUserDataEditorError)code reason:(NSString *)reasonFormatString, ... { +- (NSError *)logAndMakeSaveErrorWithCode:(BAInstallDataEditorError)code reason:(NSString *)reasonFormatString, ... { va_list arglist; va_start(arglist, reasonFormatString); NSString *reason = [[NSString alloc] initWithFormat:reasonFormatString arguments:arglist]; va_end(arglist); [BALogger publicForDomain:PUBLIC_DOMAIN message:@"%@", reason]; - return [NSError errorWithDomain:BatchUserDataEditorErrorDomain + return [NSError errorWithDomain:BAInstallDataEditorErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : reason}]; } diff --git a/Sources/Batch/Modules/User/BAUserDataManager.h b/Sources/Batch/Modules/User/BAUserDataManager.h index 67172fb..f56a121 100644 --- a/Sources/Batch/Modules/User/BAUserDataManager.h +++ b/Sources/Batch/Modules/User/BAUserDataManager.h @@ -18,22 +18,30 @@ @abstract Shared operation queue that should synchronize all access to the db. ANY SQL ACCESS should happen in this queue. */ -+ (dispatch_queue_t)sharedQueue; ++ (nonnull dispatch_queue_t)sharedQueue; // Delay is in ms + (void)startAttributesSendWSWithDelay:(long long)delay; + (void)startAttributesCheckWSWithDelay:(long long)delay; -+ (void)storeTransactionID:(NSString *)transaction forVersion:(NSNumber *)version; ++ (void)storeTransactionID:(nonnull NSString *)transaction forVersion:(nonnull NSNumber *)version; + (void)updateWithServerDataVersion:(long long)serverVersion; -+ (void)printDebugInformation; - + (void)clearData; -+ (void)addOperationQueueAndSubmit:(NSArray *)queue withCompletion:(void (^_Nullable)(void))completion; +/// Only clears attributes and tags where clearData removes versions & stuff. ++ (void)clearRemoteInstallationDataWithCompletion:(void (^_Nullable)(void))completion; + +/// Exposed for tests only ++ (void)_performClearRemoteInstallationDataWithCompletion:(void (^_Nullable)(void))completion; + +/// Can we save data? ++ (BOOL)canSave; + ++ (void)addOperationQueueAndSubmit:(NSArray *_Nonnull)queue + withCompletion:(void (^_Nullable)(void))completion; // Testing methods diff --git a/Sources/Batch/Modules/User/BAUserDataManager.m b/Sources/Batch/Modules/User/BAUserDataManager.m index 8df9bb5..2106158 100644 --- a/Sources/Batch/Modules/User/BAUserDataManager.m +++ b/Sources/Batch/Modules/User/BAUserDataManager.m @@ -178,13 +178,6 @@ + (void)updateWithServerDataVersion:(long long)serverVersion { }); } -+ (void)printDebugInformation { - dispatch_async([BAUserDataManager sharedQueue], ^{ - id datasource = [BAInjection injectProtocol:@protocol(BAUserDatasourceProtocol)]; - [datasource printDebugDump]; - }); -} - + (void)clearData { [baUserDataManagerCheckScheduledLock lock]; baUserDataManagerCheckScheduled = NO; @@ -199,6 +192,52 @@ + (void)clearData { }); } ++ (void)clearRemoteInstallationDataWithCompletion:(void (^)(void))completion { + // addOperationQueueAndSubmit is not thread safe, we need to use the queue + dispatch_async([BAUserDataManager sharedQueue], ^{ + if (![self canSave]) { + if (completion != nil) { + completion(); + } + return; + } + [BAUserDataManager _performClearRemoteInstallationDataWithCompletion:completion]; + }); +} + +/// Thread unsafe method that performs the clear without any checks +/// The only reason this exists is to write tests as BAUserDataManager is not easily testable at all ++ (void)_performClearRemoteInstallationDataWithCompletion:(void (^)(void))completion { + NSArray *applyQueue = @[ + ^BOOL { + id datasource = [BAInjection injectProtocol:@protocol(BAUserDatasourceProtocol)]; + return [datasource clearTags]; + }, + ^BOOL { + id datasource = [BAInjection injectProtocol:@protocol(BAUserDatasourceProtocol)]; + return [datasource clearAttributes]; + } + ]; + [BAUserDataManager addOperationQueueAndSubmit:applyQueue withCompletion:completion]; +} + ++ (BOOL)canSave { + if (![[[BACoreCenter instance] status] isRunning]) { + [BALogger publicForDomain:PUBLIC_DOMAIN + message:@"Batch must be started before changes to user data can be saved. The changes you've " + @"just tried to save have been discarded."]; + return false; + } + + if ([[BAOptOut instance] isOptedOut]) { + [BALogger publicForDomain:PUBLIC_DOMAIN + message:@"Batch is Opted-Out from: BatchUserDataEditor changes cannot be saved"]; + return false; + } + + return true; +} + + (BOOL)writeChangesToDatasource:(NSArray *)applyQueue changeset:(long long)changeset { id datasource = [BAInjection injectProtocol:@protocol(BAUserDatasourceProtocol)]; diff --git a/Sources/Batch/Modules/User/BAUserEmailSubscription.h b/Sources/Batch/Modules/User/BAUserEmailSubscription.h deleted file mode 100644 index b4d62a2..0000000 --- a/Sources/Batch/Modules/User/BAUserEmailSubscription.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// Batch -// -// Copyright © Batch.com. All rights reserved. -// - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -/// Enum defining the email kinds for subscription management -typedef NS_ENUM(NSUInteger, BAEmailKind) { - BAEmailKindMarketing = 0, -}; - -@interface BAUserEmailSubscription : NSObject - -- (instancetype)initWithEmail:(nullable NSString *)email; - -- (void)setEmail:(nullable NSString *)email; - -- (void)setEmailSubscriptionState:(BatchEmailSubscriptionState)state forKind:(BAEmailKind)kind; - -- (void)sendEmailSubscriptionEvent; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Batch/Modules/User/BAUserEmailSubscription.m b/Sources/Batch/Modules/User/BAUserEmailSubscription.m deleted file mode 100644 index ae06033..0000000 --- a/Sources/Batch/Modules/User/BAUserEmailSubscription.m +++ /dev/null @@ -1,108 +0,0 @@ -// -// Batch -// -// Copyright © Batch.com. All rights reserved. -// - -#import "BAUserEmailSubscription.h" -#import -#import - -#define DEBUG_DOMAIN @"BAUserEmailSubscription" - -@implementation BAUserEmailSubscription { - /// User email - NSString *_email; - - /// If we should delete the email - BOOL _deleteEmail; - - /// Subscriptions - NSMutableDictionary *_subscriptions; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _subscriptions = [NSMutableDictionary dictionary]; - } - return self; -} - -- (instancetype)initWithEmail:(nullable NSString *)email { - self = [super init]; - if (self) { - if (email == nil) { - _deleteEmail = true; - } - _email = email; - _subscriptions = [NSMutableDictionary dictionary]; - } - return self; -} - -- (void)setEmail:(nullable NSString *)email { - if (email == nil) { - _deleteEmail = true; - } - _email = email; -} - -- (void)setEmailSubscriptionState:(BatchEmailSubscriptionState)state forKind:(BAEmailKind)kind { - [_subscriptions setValue:[BAUserEmailSubscription subscriptionStateToString:state] - forKey:[BAUserEmailSubscription emailKindToString:kind]]; -} - -- (void)sendEmailSubscriptionEvent { - // Ensure we have a custom identifier - NSString *customID = [BatchUser identifier]; - if (customID == nil) { - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Custom ID nill, not sending event"]; - return; - } - - NSMutableDictionary *params = [NSMutableDictionary dictionary]; - params[@"custom_id"] = customID; - - if (_email != nil) { - params[@"email"] = _email; - } else if (_deleteEmail) { - params[@"email"] = [NSNull null]; - } - - if ([_subscriptions count] != 0) { - params[@"subscriptions"] = [_subscriptions copy]; - } - [BATrackerCenter trackPrivateEvent:@"_EMAIL_CHANGED" parameters:params]; -} - -+ (NSString *)subscriptionStateToString:(BatchEmailSubscriptionState)state { - NSString *result = nil; - switch (state) { - case BatchEmailSubscriptionStateSubscribed: - result = @"subscribed"; - break; - case BatchEmailSubscriptionStateUnsubscribed: - result = @"unsubscribed"; - break; - default: - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Unknown subscriptions state"]; - break; - } - return result; -} - -+ (NSString *)emailKindToString:(BAEmailKind)kind { - NSString *result = nil; - switch (kind) { - case BAEmailKindMarketing: - result = @"marketing"; - break; - default: - [BALogger debugForDomain:DEBUG_DOMAIN message:@"Unknown email kind"]; - break; - } - return result; -} - -@end diff --git a/Sources/Batch/PrivateUmbrellaHeader.h b/Sources/Batch/PrivateUmbrellaHeader.h index 5d7f202..15d061d 100644 --- a/Sources/Batch/PrivateUmbrellaHeader.h +++ b/Sources/Batch/PrivateUmbrellaHeader.h @@ -43,7 +43,6 @@ #import #import #import -#import #import #import #import @@ -51,10 +50,8 @@ #import #import #import -#import -#import +#import #import -#import #import #import #import @@ -123,9 +120,8 @@ #import #import #import -#import #import -#import +#import #import #import #import @@ -233,7 +229,6 @@ #import #import #import -#import #import #import #import @@ -243,7 +238,6 @@ #import #import #import -#import #import #import #import diff --git a/Sources/Batch/Versions.h b/Sources/Batch/Versions.h index 5d87c8f..cff7cf2 100644 --- a/Sources/Batch/Versions.h +++ b/Sources/Batch/Versions.h @@ -11,6 +11,6 @@ Comments should not use the // form, as the plist preprocessor will include them */ -#define BASDKVersion 1.21.2 -#define BAAPILevel 70 +#define BASDKVersion 2.0.0 +#define BAAPILevel 200 #define BAMessagingAPILevel 12 diff --git a/Sources/Batch/Webservices/BAWebserviceMetrics.h b/Sources/Batch/Webservices/BAWebserviceMetrics.h deleted file mode 100644 index 279b3c3..0000000 --- a/Sources/Batch/Webservices/BAWebserviceMetrics.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// BAWebserviceMetrics.h -// Batch -// -// https://batch.com -// Copyright (c) 2015 Batch SDK. All rights reserved. -// - -#import - -/*! - @class BAWebserviceMetrics - @abstract Manages the Webservice metrics. - */ -@interface BAWebserviceMetrics : NSObject - -/*! - @method sharedInstance - @return Returns the Webservice Metrics singleton - */ -+ (BAWebserviceMetrics *)sharedInstance; - -/*! - @method popMetrics - @return Get the metrics for a webservice as an array of dictionaries and remove them from the cache - */ -- (NSArray *)popMetricsAsDictionaries; - -/*! - @method webserviceStarted: - @abstract Starts tracking a webservice duration - @param shortName The webservice's short name - */ -- (void)webserviceStarted:(NSString *)shortName; - -/*! - @method webserviceFinished:success: - @abstract Marks the end of a webservice's execution - @param shortName The webservice's short name - @param success Whether the WS succeeded or not - */ -- (void)webserviceFinished:(NSString *)shortName success:(BOOL)success; - -@end - -/*! - @class BAWebserviceMetric - @abstract A Webservice metric representation - */ -@interface BAWebserviceMetric : NSObject - -- (instancetype)initWithShortname:(NSString *)shortName; - -@property (readonly, nonatomic) NSString *shortName; - -@property (readonly, nonatomic) NSDate *startDate; - -@property (readonly, nonatomic) NSDate *endDate; - -@property (readonly, nonatomic) BOOL success; - -- (BOOL)isFinished; - -- (NSDictionary *)dictionaryRepresentation; - -- (void)finishWithResult:(BOOL)success; - -@end \ No newline at end of file diff --git a/Sources/Batch/Webservices/BAWebserviceMetrics.m b/Sources/Batch/Webservices/BAWebserviceMetrics.m deleted file mode 100644 index e8d6a67..0000000 --- a/Sources/Batch/Webservices/BAWebserviceMetrics.m +++ /dev/null @@ -1,109 +0,0 @@ -// -// BAWebserviceMetrics.m -// Batch -// -// https://batch.com -// Copyright (c) 2015 Batch SDK. All rights reserved. -// - -#import - -@interface BAWebserviceMetrics () { - NSMutableDictionary *_metrics; -} - -@end - -@implementation BAWebserviceMetrics - -+ (BAWebserviceMetrics *)sharedInstance { - static BAWebserviceMetrics *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [BAWebserviceMetrics new]; - }); - - return instance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _metrics = [NSMutableDictionary new]; - } - return self; -} - -- (NSArray *)popMetricsAsDictionaries { - @synchronized(_metrics) { - NSMutableArray *poppedMetrics = [NSMutableArray new]; - - for (BAWebserviceMetric *metric in [_metrics allValues]) { - if ([metric isFinished]) { - [poppedMetrics addObject:[metric dictionaryRepresentation]]; - [_metrics removeObjectForKey:[metric shortName]]; - } - } - - return poppedMetrics; - } -} - -- (void)webserviceStarted:(NSString *)shortName { - if (!shortName) { - return; - } - - @synchronized(_metrics) { - [_metrics setObject:[[BAWebserviceMetric alloc] initWithShortname:shortName] forKey:shortName]; - } -} - -- (void)webserviceFinished:(NSString *)shortName success:(BOOL)success { - if (!shortName) { - return; - } - - @synchronized(_metrics) { - [[_metrics objectForKey:shortName] finishWithResult:success]; - } -} - -@end - -@implementation BAWebserviceMetric - -- (instancetype)initWithShortname:(NSString *)shortName { - self = [super init]; - if (self) { - _shortName = shortName; - _startDate = [NSDate date]; - _endDate = nil; - _success = NO; - } - return self; -} - -- (BOOL)isFinished { - return self.endDate != nil; -} - -- (NSDictionary *)dictionaryRepresentation; -{ - if (!_shortName || !_startDate || !_endDate) { - return @{}; - } - - return @{ - @"u" : self.shortName, - @"s" : [NSNumber numberWithBool:self.success], - @"t" : [NSNumber numberWithInt:(int)([self.endDate timeIntervalSinceDate:self.startDate] * 1000)] - }; -} - -- (void)finishWithResult:(BOOL)success { - _success = success; - _endDate = [NSDate date]; -} - -@end diff --git a/Sources/Batch/Webservices/Core/BAConnection.h b/Sources/Batch/Webservices/Core/BAConnection.h index 7c04135..4d91671 100644 --- a/Sources/Batch/Webservices/Core/BAConnection.h +++ b/Sources/Batch/Webservices/Core/BAConnection.h @@ -144,7 +144,7 @@ typedef NSInteger BAConnectionErrorCause; - (void)configureWithMethod:(BAConnectionMethod)method url:(nonnull NSURL *)url body:(nullable NSData *)body - cryptorFactory:(nullable id)cryptorFactory; + cryptorFactory:(nullable Class)cryptorFactory; - (void)start; diff --git a/Sources/Batch/Webservices/Core/BAURLSession.m b/Sources/Batch/Webservices/Core/BAURLSession.m index e16381d..5013af6 100644 --- a/Sources/Batch/Webservices/Core/BAURLSession.m +++ b/Sources/Batch/Webservices/Core/BAURLSession.m @@ -19,11 +19,7 @@ @implementation BAURLSession dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // Enforce TLS 1.2 - if (@available(iOS 13, tvOS 13, watchOS 6, macOS 10.15, *)) { - sessionConfig.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; - } else { - sessionConfig.TLSMinimumSupportedProtocol = kTLSProtocol12; - } + sessionConfig.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:nil delegateQueue:nil]; }); diff --git a/Sources/Batch/Webservices/Core/BAWebserviceMsgPackClient.m b/Sources/Batch/Webservices/Core/BAWebserviceMsgPackClient.m index f00c75f..8427489 100644 --- a/Sources/Batch/Webservices/Core/BAWebserviceMsgPackClient.m +++ b/Sources/Batch/Webservices/Core/BAWebserviceMsgPackClient.m @@ -9,6 +9,7 @@ #import #import +#import #define BA_METRIC_HEADER_SDK_VERSION @"x-batch-sdk-version" #define BA_METRIC_HEADER_SCHEMA_VERSION @"x-batch-protocol-version" diff --git a/Sources/Batch/Webservices/MsgPack Services implementations/BALocalCampaignsJITService.m b/Sources/Batch/Webservices/MsgPack Services implementations/BALocalCampaignsJITService.m index b323c5a..8147dac 100644 --- a/Sources/Batch/Webservices/MsgPack Services implementations/BALocalCampaignsJITService.m +++ b/Sources/Batch/Webservices/MsgPack Services implementations/BALocalCampaignsJITService.m @@ -87,11 +87,6 @@ - (nullable NSData *)requestBody:(NSError **)error { body[@"campaigns"] = campaignIds; body[@"views"] = views; - // Add system ids - NSNumber *trackerState = [NSNumber numberWithInteger:[BATrackerCenter currentMode]]; - identifiers[@"m_e"] = trackerState; - NSNumber *pushState = [[BAPushCenter instance] swizzled] ? @1 : @0; - identifiers[@"m_p"] = pushState; [identifiers addEntriesFromDictionary:[[BAStandardQueryWebserviceIdentifiersProvider sharedInstance] identifiers]]; // Add campaign ids to check and views count diff --git a/Sources/Batch/Webservices/Query Services Implementations/BAUserDataServices.m b/Sources/Batch/Webservices/Query Services Implementations/BAUserDataServices.m index 69e386f..88901ce 100644 --- a/Sources/Batch/Webservices/Query Services Implementations/BAUserDataServices.m +++ b/Sources/Batch/Webservices/Query Services Implementations/BAUserDataServices.m @@ -9,6 +9,8 @@ #import #import +#import +#import #import #import #import @@ -16,6 +18,7 @@ #import #import #import +#import #define DEFAULT_RECHECK_WAIT_TIME @(15000) @@ -81,6 +84,16 @@ - (void)webserviceClient:(BAQueryWebserviceClient *)client if ([response isKindOfClass:[BAWSResponseAttributes class]]) { BAWSResponseAttributes *castedResponse = (BAWSResponseAttributes *)response; [BAUserDataManager storeTransactionID:castedResponse.transactionID forVersion:castedResponse.version]; + if (castedResponse.projectKey != nil) { + NSString *oldProjectKey = [BAParameter objectForKey:kParametersProjectKey fallback:nil]; + if (![castedResponse.projectKey isEqualToString:oldProjectKey]) { + // If we are here this mean we are running on a fresh V2 install and user has + // just wrote some profile data. + // So we save the project key to not trigger the profile data migration from the + // next ATC response otherwise we would erase the data we just sent. + [BAParameter setValue:castedResponse.projectKey forKey:kParametersProjectKey saved:true]; + } + } } } } @@ -195,6 +208,17 @@ - (void)webserviceClient:(BAQueryWebserviceClient *)client foundValidCheckAnswer = NO; break; } + + // Checking whether project has changed + if (castedResponse.projectKey != nil) { + NSString *oldProjectKey = [BAParameter objectForKey:kParametersProjectKey fallback:nil]; + if (![castedResponse.projectKey isEqualToString:oldProjectKey]) { + [BAParameter setValue:castedResponse.projectKey forKey:kParametersProjectKey saved:true]; + [[BAInjection injectProtocol:@protocol(BAProfileCenterProtocol)] + onProjectChanged:oldProjectKey + withNewKey:castedResponse.projectKey]; + } + } } } diff --git a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.h b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.h index aa38781..3cc8ef5 100644 --- a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.h +++ b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.h @@ -33,4 +33,10 @@ */ @property (readonly) NSNumber *version; +/*! + @property projectKey + @abstract ProjectKey attached the application + */ +@property (readonly) NSString *projectKey; + @end diff --git a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.m b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.m index 86fb919..60a404b 100644 --- a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.m +++ b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributes.m @@ -27,6 +27,10 @@ - (instancetype)initWithResponse:(NSDictionary *)response { _version = nil; } + _projectKey = [response objectForKey:@"project_key"]; + if ([BANullHelper isStringEmpty:_projectKey]) { + _projectKey = nil; + } return self; } diff --git a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.h b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.h index aca1c0a..16389b2 100644 --- a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.h +++ b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.h @@ -54,4 +54,10 @@ typedef NS_ENUM(NSUInteger, BAWSResponseAttrCheckAction) { */ @property (readonly) NSNumber *time; +/*! + @property projectKey + @abstract ProjectKey attached the application + */ +@property (readonly) NSString *projectKey; + @end diff --git a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.m b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.m index c324523..839e345 100644 --- a/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.m +++ b/Sources/Batch/Webservices/Query Services Implementations/Response Models/BAWSResponseAttributesCheck.m @@ -21,6 +21,7 @@ - (instancetype)initWithResponse:(NSDictionary *)response { _actionString = [response objectForKey:@"action"]; _version = [response objectForKey:@"ver"]; _time = [response objectForKey:@"t"]; + _projectKey = [response objectForKey:@"project_key"]; // Sanity checks yay if (![_actionString isKindOfClass:[NSString class]]) { @@ -35,6 +36,9 @@ - (instancetype)initWithResponse:(NSDictionary *)response { _time = nil; } + if ([BANullHelper isStringEmpty:_projectKey]) { + _projectKey = nil; + } return self; } diff --git a/Sources/Batch/Webservices/Query/BAQueryWebserviceClient.m b/Sources/Batch/Webservices/Query/BAQueryWebserviceClient.m index 8cb74df..0ac9f3b 100644 --- a/Sources/Batch/Webservices/Query/BAQueryWebserviceClient.m +++ b/Sources/Batch/Webservices/Query/BAQueryWebserviceClient.m @@ -9,7 +9,6 @@ #import #import -#import #import #import @@ -153,10 +152,6 @@ - (NSArray *)webserviceResponsesFromResponse:(NSDictionary *)rawResponse { return responses; } -- (void)updateMetricWithSuccess:(BOOL)success { - [[BAWebserviceMetrics sharedInstance] webserviceFinished:self.shortIdentifier success:success]; -} - #pragma mark - #pragma mark BAWebserviceClient overrides @@ -180,12 +175,6 @@ - (NSMutableDictionary *)requestBodyDictionary { NSMutableDictionary *identifiers = [NSMutableDictionary new]; postParameters[@"ids"] = identifiers; - // Modules - NSNumber *trackerState = [NSNumber numberWithInteger:[BATrackerCenter currentMode]]; - identifiers[@"m_e"] = trackerState; - NSNumber *pushState = [[BAPushCenter instance] swizzled] ? @1 : @0; - identifiers[@"m_p"] = pushState; - // Add common identifiers [identifiers addEntriesFromDictionary:[_identifiersProvider identifiers]]; @@ -199,20 +188,6 @@ - (NSMutableDictionary *)requestBodyDictionary { postParameters[@"nath"] = notificationAuthorization; } - // NSLog(@"DebugOptin: ts: %@ dty: %@ nath: %@", [NSDate new], [BAPropertiesCenter valueForShortName:@"nty"], - // [[notificationAuthorization description] stringByReplacingOccurrencesOfString:@"\n" withString:@""]); - - @try { - // Add the webservice metrics - NSArray *metrics = [[BAWebserviceMetrics sharedInstance] popMetricsAsDictionaries]; - if ([metrics count] > 0) { - postParameters[@"metrics"] = metrics; - } - } @catch (NSException *metricException) { - [BALogger errorForDomain:@"BatchWebserviceMetrics" - message:@"Error while adding metrics to webservice: %@", [metricException description]]; - } - return postParameters; } @@ -230,12 +205,10 @@ - (void)connectionWillStart { if ([self.delegate respondsToSelector:@selector(webserviceClientWillStart:)]) { [self.delegate webserviceClientWillStart:self]; } - [[BAWebserviceMetrics sharedInstance] webserviceStarted:self.shortIdentifier]; } - (void)connectionFailedWithError:(NSError *)error { [super connectionFailedWithError:error]; - [self updateMetricWithSuccess:false]; [BALogger errorForDomain:LOCAL_ERROR_DOMAIN @@ -259,7 +232,6 @@ - (void)connectionDidFinishLoadingWithData:(NSData *)data { [super connectionDidFinishLoadingWithData:data]; // Check data. if ([BANullHelper isNull:data]) { - [self updateMetricWithSuccess:false]; [BALogger errorForDomain:LOCAL_ERROR_DOMAIN message:@"%@ webservice return a NULL or empty data.", self.datasource.requestIdentifier]; [self.delegate webserviceClient:self didFailWithError:[BAErrorHelper webserviceError]]; @@ -271,7 +243,6 @@ - (void)connectionDidFinishLoadingWithData:(NSData *)data { NSDictionary *startDict = [BAJson deserializeDataAsDictionary:data error:nil]; if (![BANullHelper isNull:startDict]) { - [self updateMetricWithSuccess:true]; [self handleResponse:startDict]; } else { [[NSException exceptionWithName:@"Invalid content." @@ -280,7 +251,6 @@ - (void)connectionDidFinishLoadingWithData:(NSData *)data { userInfo:nil] raise]; } } @catch (NSException *exception) { - [self updateMetricWithSuccess:false]; [BALogger errorForDomain:LOCAL_ERROR_DOMAIN message:@"Error %@ webservice: %@", self.datasource.requestIdentifier, [exception description]]; diff --git a/Sources/Batch/Webservices/Query/BAQueryWebserviceClientDelegate.h b/Sources/Batch/Webservices/Query/BAQueryWebserviceClientDelegate.h index a818d99..908c107 100644 --- a/Sources/Batch/Webservices/Query/BAQueryWebserviceClientDelegate.h +++ b/Sources/Batch/Webservices/Query/BAQueryWebserviceClientDelegate.h @@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN @protocol BAQueryWebserviceClientDelegate +@optional + - (void)webserviceClientWillStart:(BAQueryWebserviceClient *)client; @required diff --git a/Sources/Batch/Webservices/Query/BAStandardQueryWebserviceIdentifiersProvider.m b/Sources/Batch/Webservices/Query/BAStandardQueryWebserviceIdentifiersProvider.m index efc9274..3d1486b 100644 --- a/Sources/Batch/Webservices/Query/BAStandardQueryWebserviceIdentifiersProvider.m +++ b/Sources/Batch/Webservices/Query/BAStandardQueryWebserviceIdentifiersProvider.m @@ -11,6 +11,7 @@ #import #import #import +#import @implementation BAStandardQueryWebserviceIdentifiersProvider @@ -24,46 +25,7 @@ + (instancetype)sharedInstance { } - (NSDictionary *)identifiers { - NSMutableDictionary *ids = [[NSMutableDictionary alloc] init]; - // Always add the installation id - NSString *di = [BAPropertiesCenter valueForShortName:@"di"]; - if (![BANullHelper isStringEmpty:di]) { - ids[@"di"] = di; - } - - // Grab the identifiers list. - NSString *idsList = [BAParameter objectForKey:kParametersIDsPatternKey fallback:kParametersIDsPatternValue]; - NSArray *baseIds = [idsList componentsSeparatedByString:@","]; - - NSString *advancedIdsList = [BAParameter objectForKey:kParametersAdvancedIDsPatternKey - fallback:kParametersAdvancedIDsPatternValue]; - NSArray *advancedIds = nil; - - if (![BANullHelper isStringEmpty:advancedIdsList] && - [[BACoreCenter instance].configuration useAdvancedDeviceInformation]) { - advancedIds = [advancedIdsList componentsSeparatedByString:@","]; - } - - NSMutableArray *idsToFetch = - [[NSMutableArray alloc] initWithCapacity:(baseIds.count + advancedIds.count)]; - if (baseIds) { - [idsToFetch addObjectsFromArray:baseIds]; - } - if (advancedIds) { - [idsToFetch addObjectsFromArray:advancedIds]; - } - - // Add references. - for (NSString *idName in idsToFetch) { - if (![BANullHelper isStringEmpty:idName]) { - NSString *propertyValue = [BAPropertiesCenter valueForShortName:idName]; - if (![BANullHelper isStringEmpty:propertyValue]) { - [ids setObject:propertyValue forKey:idName]; - } - } - } - - return ids; + return [[BATDataCollectionCenter sharedInstance] buildIdsForQuery]; } @end diff --git a/Sources/batchTests/Kernel/GZip/gzipTests.m b/Sources/batchTests/Kernel/GZip/gzipTests.m index 8f17dd0..3ad31f9 100644 --- a/Sources/batchTests/Kernel/GZip/gzipTests.m +++ b/Sources/batchTests/Kernel/GZip/gzipTests.m @@ -149,7 +149,7 @@ // Tweaked to have 1 as a timestamp rather than 0 to match libz NSString *base64HelloWorld = @"H4sIAAAAAAAAE/NIzcnJV3BKLEnOAADB8muICwAAAA=="; -static NSData *createRandomNSData() { +static NSData *createRandomNSData(void) { NSUInteger size = 10 * 1024 * 1024; // 10mb NSMutableData *data = [NSMutableData dataWithLength:size]; u_int32_t *bytes = (u_int32_t *)data.mutableBytes; diff --git a/Sources/batchTests/Kernel/Helpers/emailTests.swift b/Sources/batchTests/Kernel/Helpers/emailTests.swift deleted file mode 100644 index 3ff309e..0000000 --- a/Sources/batchTests/Kernel/Helpers/emailTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// BatchTests -// -// Copyright © Batch.com. All rights reserved. -// - -import Batch.Batch_Private -import XCTest - -final class emailTests: XCTestCase { - func testEmailPatterns() { - XCTAssertTrue(BAEmailUtils.isValidEmail("foo@batch.com")) - XCTAssertTrue(BAEmailUtils.isValidEmail("bar@foo.batch.com")) - XCTAssertTrue(BAEmailUtils.isValidEmail("bar+foo@batch.com")) - XCTAssertTrue(BAEmailUtils.isValidEmail("FOObar@Test.Batch.COM")) - - XCTAssertFalse(BAEmailUtils.isValidEmail("@gmail.com")) - XCTAssertFalse(BAEmailUtils.isValidEmail("invalid@gmail")) - XCTAssertFalse(BAEmailUtils.isValidEmail("inva lid@gmail.com")) - XCTAssertFalse(BAEmailUtils.isValidEmail("invalid@gmail .com")) - XCTAssertFalse(BAEmailUtils.isValidEmail("invalid@inva lid.gmail.com")) - } - - func testEmailIsTooLong() { - XCTAssertTrue(BAEmailUtils.isEmailTooLong("testastringtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoo@batch.com")) - XCTAssertFalse(BAEmailUtils.isEmailTooLong("bar@foo.batch.com")) - } -} diff --git a/Sources/batchTests/Kernel/Helpers/swizzlingTests.m b/Sources/batchTests/Kernel/Helpers/swizzlingTests.m index f27a4c1..d8368b4 100644 --- a/Sources/batchTests/Kernel/Helpers/swizzlingTests.m +++ b/Sources/batchTests/Kernel/Helpers/swizzlingTests.m @@ -214,7 +214,7 @@ - (void)testNonoptionalMethods { XCTAssertEqual(1, [applicationDelegate.invokedSelectors count]); // Optional methods aren't supposed to be implemented, so we expect less calls than all // that's supported by the delegate - XCTAssertEqual(4, [batchDelegateProxy.proxy_invokedSelectors count]); + XCTAssertEqual(2, [batchDelegateProxy.proxy_invokedSelectors count]); } - (void)testAllMethods { @@ -240,8 +240,8 @@ - (void)testAllMethods { XCTAssertTrue([delegatedAppDelegate swizzleAppDelegate]); [self callAllDelegateMethodsOn:applicationDelegate applicationMock:uiApplicationMock]; - XCTAssertEqual(7, [applicationDelegate.invokedSelectors count]); - XCTAssertEqual(7, [batchDelegateProxy.proxy_invokedSelectors count]); + XCTAssertEqual(2, [applicationDelegate.invokedSelectors count]); + XCTAssertEqual(2, [batchDelegateProxy.proxy_invokedSelectors count]); } - (void)callAllDelegateMethodsOn:(id)delegate applicationMock:(UIApplication *)application { @@ -252,44 +252,6 @@ - (void)callAllDelegateMethodsOn:(id)delegate application if ([delegate respondsToSelector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)]) { [delegate application:application didFailToRegisterForRemoteNotificationsWithError:[self dummyError]]; } - - if ([delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) { - [delegate application:application - didReceiveRemoteNotification:[NSDictionary new] - fetchCompletionHandler:^(UIBackgroundFetchResult result){ - }]; - } - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if ([delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:)]) { - [delegate application:application didReceiveRemoteNotification:[NSDictionary new]]; - } - - if ([delegate respondsToSelector:@selector(application:didRegisterUserNotificationSettings:)]) { - id userNotificationSettings = OCMClassMock([UIUserNotificationSettings class]); - [delegate application:application didRegisterUserNotificationSettings:userNotificationSettings]; - } - - if ([delegate respondsToSelector:@selector(application: - handleActionWithIdentifier:forRemoteNotification:completionHandler:)]) { - [delegate application:application - handleActionWithIdentifier:@"foo" - forRemoteNotification:[NSDictionary new] - completionHandler:^{ - }]; - } - - if ([delegate respondsToSelector:@selector - (application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:)]) { - [delegate application:application - handleActionWithIdentifier:@"foo" - forRemoteNotification:[NSDictionary new] - withResponseInfo:[NSDictionary new] - completionHandler:^{ - }]; - } -#pragma clang diagnostic pop } - (NSError *)dummyError { @@ -383,39 +345,9 @@ - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotif _didFailToRegisterRecorded = true; } -- (void)application:(nonnull UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { -} - -- (void)application:(nonnull UIApplication *)application - didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo - fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler { -} - - (void)application:(nonnull UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(nonnull NSData *)deviceToken { _didRegisterForRemoteNotificationsRecorded = true; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (void)application:(nonnull UIApplication *)application - didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings { -} - -#pragma clang diagnostic pop - -- (void)application:(nonnull UIApplication *)application - handleActionWithIdentifier:(nullable NSString *)identifier - forRemoteNotification:(nonnull NSDictionary *)userInfo - completionHandler:(nonnull void (^)(void))completionHandler { -} - -- (void)application:(nonnull UIApplication *)application - handleActionWithIdentifier:(nullable NSString *)identifier - forRemoteNotification:(nonnull NSDictionary *)userInfo - withResponseInfo:(nonnull NSDictionary *)responseInfo - completionHandler:(nonnull void (^)(void))completionHandler { -} - @end diff --git a/Sources/batchTests/Kernel/Logger/internalLoggerTests.swift b/Sources/batchTests/Kernel/Logger/internalLoggerTests.swift index b58034a..4f70015 100644 --- a/Sources/batchTests/Kernel/Logger/internalLoggerTests.swift +++ b/Sources/batchTests/Kernel/Logger/internalLoggerTests.swift @@ -17,13 +17,13 @@ class internalLoggerTests: XCTestCase { override func setUp() { loggerDelegate.reset() - Batch.setLoggerDelegate(loggerDelegate) + BatchSDK.loggerDelegate = loggerDelegate } override class func tearDown() { // Leave the internal logs true for other tests BALogger.internalLogsEnabled = true - Batch.setLoggerDelegate(nil) + BatchSDK.loggerDelegate = nil } func testEnableInternalLogsWithAPI() { diff --git a/Sources/batchTests/Mocks/BatchUserDataEditorMock.swift b/Sources/batchTests/Mocks/BatchUserDataEditorMock.swift new file mode 100644 index 0000000..c24ba42 --- /dev/null +++ b/Sources/batchTests/Mocks/BatchUserDataEditorMock.swift @@ -0,0 +1,36 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import Foundation + +class BAInstallDataEditorMock: BAInstallDataEditor { + var setIdentifierCalled: Bool = false + var identifierToSet: String? + var saveCalled: Bool = false + + override init() { + super.init() + self.reset() + } + + override func setIdentifier(_ identifier: String?) { + setIdentifierCalled = true + identifierToSet = identifier + } + + override func save() { + // Do nothing + saveCalled = true + } + + func reset() { + setIdentifierCalled = false + identifierToSet = nil + saveCalled = false + } +} diff --git a/Sources/batchTests/Stubs/DeeplinkDelegateStub.h b/Sources/batchTests/Mocks/DeeplinkDelegateStub.h similarity index 100% rename from Sources/batchTests/Stubs/DeeplinkDelegateStub.h rename to Sources/batchTests/Mocks/DeeplinkDelegateStub.h diff --git a/Sources/batchTests/Stubs/DeeplinkDelegateStub.m b/Sources/batchTests/Mocks/DeeplinkDelegateStub.m similarity index 100% rename from Sources/batchTests/Stubs/DeeplinkDelegateStub.m rename to Sources/batchTests/Mocks/DeeplinkDelegateStub.m diff --git a/Sources/batchTests/Mocks/EventTrackerMock.swift b/Sources/batchTests/Mocks/EventTrackerMock.swift new file mode 100644 index 0000000..cc8a63e --- /dev/null +++ b/Sources/batchTests/Mocks/EventTrackerMock.swift @@ -0,0 +1,72 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import Foundation + +@objc +class MockEventTracker: NSObject, BATEventTracker { + var trackedEvents: [BAEvent] = [] + + func trackPublicEvent(name: String, attributes: BatchEventAttributes?) { + var parameters = [AnyHashable: Any]() + if let attributes { + parameters = try! BATEventAttributesSerializer.serialize(eventAttributes: attributes) + } + trackPrivateEvent(name: "E.\(name.uppercased())", parameters: parameters, collapsable: false) + } + + func trackLocation(_: CLLocation) { + // TODO: Implement this + } + + func trackPrivateEvent(name: String, parameters: [AnyHashable: Any]?, collapsable _: Bool) { + trackedEvents.append(BAEvent(name: name, andParameters: parameters)) + } + + func trackManualPrivateEvent(_ event: BAEvent) { + trackedEvents.append(event) + } + + @objc + func reset() { + trackedEvents.removeAll() + } + + // Find an event by its name and parameters + // If `parameters` is nil, all events of this name will be matched. + // To match events without parameters, provide an empty dictionary + func findEvent(name: BATInternalEvent, parameters: [AnyHashable: Any]?) -> BAEvent? { + return findEvent(name: name.rawValue, parameters: parameters) + } + + // Find an event by its name and parameters + // If `parameters` is nil, all events of this name will be matched. + // To match events without parameters, provide an empty dictionary + @objc + func findEvent(name: String, parameters: [AnyHashable: Any]?) -> BAEvent? { + return trackedEvents.filter { event in + if event.name != name { + return false + } + + if let parameters { + let eventParameters = (event.parametersDictionary ?? [:]) as NSDictionary + if !eventParameters.isEqual(to: parameters) { + return false + } + } + + return true + }.first + } + + @objc + func registerOverlay() -> BAOverlayedInjectable { + return BAInjection.overlayProtocol(BATEventTracker.self, returnedInstance: self) + } +} diff --git a/Sources/batchTests/Modules/Actions/builtinActionsTest.m b/Sources/batchTests/Modules/Actions/builtinActionsTest.m index e7c295a..5cb8c27 100644 --- a/Sources/batchTests/Modules/Actions/builtinActionsTest.m +++ b/Sources/batchTests/Modules/Actions/builtinActionsTest.m @@ -1,6 +1,7 @@ #import #import #import "BAActionsCenter.h" +#import "BatchTests-Swift.h" #import "DeeplinkDelegateStub.h" #import "OCMock.h" @@ -10,23 +11,28 @@ @interface builtinActionsTest : XCTestCase @implementation builtinActionsTest - (void)tearDown { - [Batch setDeeplinkDelegate:nil]; + [BatchSDK setDeeplinkDelegate:nil]; } - (void)testTagEditAction { - id batchUserMock = OCMClassMock([BatchUser class]); - id batchUserDataEditorMock = OCMClassMock([BatchUserDataEditor class]); - [batchUserDataEditorMock setExpectationOrderMatters:YES]; + id batchProfileMock = OCMClassMock([BatchProfile class]); + id batchProfileDataEditorMock = OCMClassMock([BatchProfileEditor class]); + [batchProfileDataEditorMock setExpectationOrderMatters:YES]; - OCMExpect([batchUserMock editor]).andReturn(batchUserDataEditorMock); - OCMExpect([batchUserDataEditorMock addTag:@"foo" inCollection:@"bar"]); - OCMExpect([batchUserDataEditorMock save]); + OCMExpect([batchProfileMock editor]).andReturn(batchProfileDataEditorMock); + OCMExpect([batchProfileDataEditorMock addItemToStringArrayAttribute:@"foo" + forKey:@"bar" + error:[OCMArg anyObjectRef]]); + OCMExpect([batchProfileDataEditorMock save]); - OCMExpect([batchUserMock editor]).andReturn(batchUserDataEditorMock); - OCMExpect([(BatchUserDataEditor *)batchUserDataEditorMock removeTag:@"foo" fromCollection:@"bar"]); - OCMExpect([batchUserDataEditorMock save]); + OCMExpect([batchProfileMock editor]).andReturn(batchProfileDataEditorMock); + OCMExpect([(BatchProfileEditor *)batchProfileDataEditorMock + removeItemFromStringArrayAttribute:@"foo" + forKey:@"bar" + error:[OCMArg anyObjectRef]]); + OCMExpect([batchProfileDataEditorMock save]); - OCMReject([batchUserMock editor]); + OCMReject([batchProfileMock editor]); [[BAActionsCenter instance] performAction:@"batch.user.tag" withArgs:@{@"c" : @"bar", @"t" : @"foo", @"a" : @"add"} @@ -67,13 +73,13 @@ - (void)testTagEditAction { andSource:nil]; [[BAActionsCenter instance] performAction:@"batch.user.tag" withArgs:@{@"t" : @"foo", @"a" : @"add"} andSource:nil]; - OCMVerifyAll(batchUserMock); - OCMVerifyAll(batchUserDataEditorMock); + OCMVerifyAll(batchProfileMock); + OCMVerifyAll(batchProfileDataEditorMock); } - (void)testDeeplinkFromActions { DeeplinkDelegateStub *delegate = [DeeplinkDelegateStub new]; - Batch.deeplinkDelegate = delegate; + BatchSDK.deeplinkDelegate = delegate; id uiApplicationMock = OCMClassMock([UIApplication class]); OCMStub([uiApplicationMock sharedApplication]).andReturn(uiApplicationMock); @@ -97,83 +103,52 @@ - (void)rejectOpenURL:(id)mock { } - (void)testTrackEventAction { - id batchUserMock = OCMClassMock([BatchUser class]); - [batchUserMock setExpectationOrderMatters:YES]; - - OCMExpect([batchUserMock trackEvent:@"test_event" - withLabel:@"test_label" - associatedData:[OCMArg checkWithBlock:^BOOL(id parameter) { - if (![parameter isKindOfClass:[BatchEventData class]]) { - return NO; - } - - BatchEventData *data = parameter; - return [data->_attributes count] == 0 && [data->_tags count] == 0; - }]]); + self.continueAfterFailure = false; + + MockEventTracker *eventTracker = [MockEventTracker new]; + [BAInjection overlayProtocol:@protocol(BATEventTrackerProtocol) + callback:^id _Nullable(id _Nullable originalInstance) { + return eventTracker; + }]; + // Register a temporary ProfileCenter so that the event tracker is not cached + [BAInjection overlayProtocol:@protocol(BAProfileCenterProtocol) + callback:^id _Nullable(id _Nullable originalInstance) { + return [[BAProfileCenter alloc] init]; + }]; + + /*OCMExpect([batchUserMock trackEvent:@"test_event" + withLabel:@"test_label" + associatedData:[OCMArg checkWithBlock:^BOOL(id parameter) { + if (![parameter isKindOfClass:[BatchEventData class]]) { + return NO; + } + + BatchEventData *data = parameter; + return [data->_attributes count] == 0 && [data->_tags count] == 0; + }]]);*/ [[BAActionsCenter instance] performAction:@"batch.user.event" withArgs:@{@"e" : @"test_event", @"l" : @"test_label"} andSource:nil]; - OCMExpect([batchUserMock trackEvent:@"test_event_2" - withLabel:@"test_label_2" - associatedData:[OCMArg checkWithBlock:^BOOL(id parameter) { - if (![parameter isKindOfClass:[BatchEventData class]]) { - return NO; - } - - BatchEventData *data = parameter; - if ([data->_attributes count] != 0) { - return NO; - } - - return [data->_tags count] == 3 && [data->_tags containsObject:@"tag1"] && - [data->_tags containsObject:@"tag2"] && [data->_tags containsObject:@"tag3"]; - }]]); + BAEvent *event = [eventTracker findEventWithName:@"E.TEST_EVENT" parameters:nil]; + XCTAssertNotNil(event); + XCTAssertEqualObjects(event.parametersDictionary[@"label"], @"test_label"); + + [eventTracker reset]; [[BAActionsCenter instance] performAction:@"batch.user.event" withArgs:@{@"e" : @"test_event_2", @"l" : @"test_label_2", @"t" : @[ @"tag1", @"tag2", @"tag3" ]} andSource:nil]; - OCMExpect([batchUserMock trackEvent:@"test_event_3" - withLabel:@"test_label_3" - associatedData:[OCMArg checkWithBlock:^BOOL(id parameter) { - if (![parameter isKindOfClass:[BatchEventData class]]) { - return NO; - } - - BatchEventData *data = parameter; - if ([data->_attributes count] != 5) { - return NO; - } - - BATTypedEventAttribute *boolAttr = [data->_attributes objectForKey:@"bool"]; - if (boolAttr.type != BAEventAttributeTypeBool || ![boolAttr.value isEqual:@YES]) { - return NO; - } - - BATTypedEventAttribute *intAttr = [data->_attributes objectForKey:@"int"]; - if (intAttr.type != BAEventAttributeTypeInteger || ![intAttr.value isEqual:@64]) { - return NO; - } - - BATTypedEventAttribute *doubleAttr = [data->_attributes objectForKey:@"double"]; - if (doubleAttr.type != BAEventAttributeTypeDouble || ![doubleAttr.value isEqual:@654.21]) { - return NO; - } - - BATTypedEventAttribute *stringAttr = [data->_attributes objectForKey:@"string"]; - if (stringAttr.type != BAEventAttributeTypeString || ![stringAttr.value isEqual:@"toto"]) { - return NO; - } - - BATTypedEventAttribute *dateAttr = [data->_attributes objectForKey:@"date"]; - if (dateAttr.type != BAEventAttributeTypeDate) { - return NO; - } - - NSNumber *timestampValue = (NSNumber *)dateAttr.value; - return [timestampValue isEqual:@1596975143943]; - }]]); + event = [eventTracker findEventWithName:@"E.TEST_EVENT_2" parameters:nil]; + XCTAssertNotNil(event); + XCTAssertEqualObjects(event.parametersDictionary[@"label"], @"test_label_2"); + NSArray *expectedTags = @[ @"tag1", @"tag2", @"tag3" ]; + XCTAssertEqualObjects((NSArray *)[event.parametersDictionary[@"tags"] + sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)], + expectedTags); + + [eventTracker reset]; [[BAActionsCenter instance] performAction:@"batch.user.event" withArgs:@{ @"e" : @"test_event_3", @@ -188,7 +163,18 @@ - (void)testTrackEventAction { } andSource:nil]; - OCMVerifyAll(batchUserMock); + event = [eventTracker findEventWithName:@"E.TEST_EVENT_3" parameters:nil]; + XCTAssertNotNil(event); + XCTAssertEqualObjects(event.parametersDictionary[@"label"], @"test_label_3"); + + NSDictionary *expectedAttibutes = @{ + @"bool.b" : @(true), + @"date.t" : @(1596975143943), + @"double.f" : @(654.21), + @"int.i" : @(64), + @"string.s" : @"toto", + }; + XCTAssertEqualObjects(event.parametersDictionary[@"attributes"], expectedAttibutes); } - (void)waitForMainThreadLoop { diff --git a/Sources/batchTests/Modules/Core/batchConfigurationTests.m b/Sources/batchTests/Modules/Core/batchConfigurationTests.m index cd60091..913a80d 100644 --- a/Sources/batchTests/Modules/Core/batchConfigurationTests.m +++ b/Sources/batchTests/Modules/Core/batchConfigurationTests.m @@ -26,57 +26,6 @@ - (void)tearDown { [super tearDown]; } -- (void)testDevelopperKey { - // Test instantiation. - BAConfiguration *c = [[BAConfiguration alloc] init]; - XCTAssertNotNil(c, @"Failed to instantiate a BAConfiguration."); - - // Test the default DevelopperKey value. - XCTAssertNil([c developperKey], @"Default developper key value is not NULL."); - - NSError *e = nil; - - // Test set a NULL value. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wall" - e = [c setDevelopperKey:nil]; -#pragma clang diagnostic pop - XCTAssertNotNil(e, @"Check NULL developper key failled."); - - // Test set an empty value. - e = [c setDevelopperKey:@""]; - XCTAssertNotNil(e, @"Check empty developper key failled."); - - // Test with a regular developper key. - e = [c setDevelopperKey:@"MYDEVKEY"]; - XCTAssertNil(e, @"Failed to set a regular developper key: %@", e); - XCTAssertNotNil([c developperKey], @"Storing a regular developper key stored a NULL."); - XCTAssertEqual([c developperKey], @"MYDEVKEY", @"Stored develloper key do not match the input."); - - BOOL mode; - // Test dev mode. - mode = [c developmentMode]; - XCTAssertFalse(mode, @"Dev key is not supposed to give a dev mode at YES."); - - // Test another key. - e = [c setDevelopperKey:@"devKEY"]; - XCTAssertNil(e, @"Failed to set a regular developper key: %@", e); - mode = [c developmentMode]; - XCTAssertFalse(mode, @"Dev key is not supposed to give a dev mode at YES."); - - // Test another key. - e = [c setDevelopperKey:@"DEV"]; - XCTAssertNil(e, @"Failed to set a regular developper key: %@", e); - mode = [c developmentMode]; - XCTAssertTrue(mode, @"Dev key is supposed to give a dev mode at YES."); - - // Test a valid key. - e = [c setDevelopperKey:@"DEVKEY"]; - XCTAssertNil(e, @"Failed to set a regular developper key: %@", e); - mode = [c developmentMode]; - XCTAssertTrue(mode, @"Dev key is supposed to give a dev mode at YES."); -} - - (void)testAssociatedDomains { BAConfiguration *config = [[BAConfiguration alloc] init]; XCTAssertNil([config associatedDomains], @"Associated domains should be nil"); diff --git a/Sources/batchTests/Modules/Core/batchCoreCenterTests.m b/Sources/batchTests/Modules/Core/batchCoreCenterTests.m index 78bd748..6aefd86 100644 --- a/Sources/batchTests/Modules/Core/batchCoreCenterTests.m +++ b/Sources/batchTests/Modules/Core/batchCoreCenterTests.m @@ -9,6 +9,7 @@ #import #import "BACoreCenter.h" +#import "BatchTests-Swift.h" #import "OCMock.h" @interface BatchCoreCenterTests : XCTestCase @@ -47,11 +48,15 @@ - (void)testUniversalLinks { [[BACoreCenter instance] openDeeplink:@"https://apple.fr/test?id=3" inApp:YES]; [[BACoreCenter instance] openDeeplink:@"https://apple.fr/test?id=3" inApp:NO]; + + [self waitForMainThreadLoop]; + OCMVerify(times(2), [uiApplicationDelegateMock application:[OCMArg any] continueUserActivity:[OCMArg any] restorationHandler:[OCMArg any]]); [[BACoreCenter instance] openDeeplink:@"https://www.apple.fr/test?id=3" inApp:YES]; + [self waitForMainThreadLoop]; OCMReject([uiApplicationDelegateMock application:[OCMArg any] continueUserActivity:[OCMArg any] restorationHandler:[OCMArg any]]); diff --git a/Sources/batchTests/Modules/Core/batchOptOutTests.m b/Sources/batchTests/Modules/Core/batchOptOutTests.m index 622519a..148d997 100644 --- a/Sources/batchTests/Modules/Core/batchOptOutTests.m +++ b/Sources/batchTests/Modules/Core/batchOptOutTests.m @@ -66,23 +66,23 @@ - (void)testPublicAPI { id optOutMock = OCMClassMock([BAOptOut class]); OCMStub([optOutMock instance]).andReturn(optOutMock); - [Batch optOut]; + [BatchSDK optOut]; OCMVerify([optOutMock setOptedOut:true wipeData:false completionHandler:[OCMArg isNotNil]]); - [Batch optIn]; + [BatchSDK optIn]; OCMVerify([optOutMock setOptedOut:false wipeData:false completionHandler:[OCMArg isNotNil]]); - [Batch optOutAndWipeData]; + [BatchSDK optOutAndWipeData]; OCMVerify([optOutMock setOptedOut:true wipeData:true completionHandler:[OCMArg isNotNil]]); id userCompletionHandler = ^BatchOptOutNetworkErrorPolicy(BOOL success) { return BatchOptOutNetworkErrorPolicyCancel; }; - [Batch optOutWithCompletionHandler:userCompletionHandler]; + [BatchSDK optOutWithCompletionHandler:userCompletionHandler]; OCMVerify([optOutMock setOptedOut:true wipeData:false completionHandler:userCompletionHandler]); - [Batch optOutAndWipeDataWithCompletionHandler:userCompletionHandler]; + [BatchSDK optOutAndWipeDataWithCompletionHandler:userCompletionHandler]; OCMVerify([optOutMock setOptedOut:true wipeData:true completionHandler:userCompletionHandler]); } @@ -147,9 +147,11 @@ - (void)testWipeData { NSString *filePath = ((BALocalCampaignsFilePersistence *)[campaignsCenter campaignPersister]).filePath.path; XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:filePath]); - // Add view event - NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970]; - [[campaignsCenter viewTracker] trackEventForCampaignID:@"campaign_id" kind:BALocalCampaignTrackerEventKindView]; + // Add view event. Remove 2 seconds from current time to reduce test flakiness + NSTimeInterval timestamp = [[[NSDate date] dateByAddingTimeInterval:-2] timeIntervalSince1970]; + BALocalCampaignCountedEvent *viewEvent = + [[campaignsCenter viewTracker] trackEventForCampaignID:@"campaign_id" kind:BALocalCampaignTrackerEventKindView]; + XCTAssertNotNil(viewEvent, @"Failed to track view event"); XCTAssertEqual([[campaignsCenter viewTracker] numberOfViewEventsSince:timestamp].intValue, 1); [[BAOptOut instance] setOptedOut:true diff --git a/Sources/batchTests/Modules/Core/batchUserProfileTests.m b/Sources/batchTests/Modules/Core/batchUserProfileTests.m deleted file mode 100644 index afa17f0..0000000 --- a/Sources/batchTests/Modules/Core/batchUserProfileTests.m +++ /dev/null @@ -1,244 +0,0 @@ -// -// batchUserProfileTests.m -// Batch -// -// https://batch.com -// Copyright (c) 2014 Batch SDK. All rights reserved. -// - -#import -#import -#import "OCMock.h" - -#import "BAParameter.h" -#import "BAUserProfile.h" -#import "BatchCore.h" -#import "BatchUserProfile.h" - -@interface MockedBAParameter : NSObject -@end - -@implementation MockedBAParameter { - NSMutableDictionary *_backingDict; -} - -- (instancetype)init { - self = [super self]; - if (self) { - _backingDict = [NSMutableDictionary new]; - } - return self; -} - -- (id)objectForKey:(NSString *)key fallback:(id)fallback { - id val = _backingDict[key]; - return val != nil ? val : fallback; -} - -- (NSError *)setValue:(id)value forKey:(NSString *)key saved:(BOOL)save { - if (key == nil) { - return [NSError errorWithDomain:@"tests" code:0 userInfo:nil]; - } - - if (value == nil) { - return [NSError errorWithDomain:@"tests" code:1 userInfo:nil]; - } - - [_backingDict setObject:value forKey:key]; - - return nil; -} - -- (NSError *)removeObjectForKey:(NSString *)key { - [_backingDict removeObjectForKey:key]; - return nil; -} - -@end - -@interface batchUserProfileTests : XCTestCase - -@end - -@implementation batchUserProfileTests { - MockedBAParameter *mockedBaParameterImpl; - id baParameterMock; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - -- (void)setUp { - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. - - // Mock the BAParameter class, because NSUserDefaults is a broken pile of shit on the simulator - - mockedBaParameterImpl = [MockedBAParameter new]; - baParameterMock = OCMClassMock([BAParameter class]); - - OCMStub(ClassMethod([baParameterMock objectForKey:[OCMArg any] fallback:[OCMArg any]])) - .andCall(mockedBaParameterImpl, @selector(objectForKey:fallback:)); - OCMStub(ClassMethod([baParameterMock setValue:[OCMArg any] forKey:[OCMArg any] saved:YES])) - .andCall(mockedBaParameterImpl, @selector(setValue:forKey:saved:)); - OCMStub(ClassMethod([baParameterMock setValue:[OCMArg any] forKey:[OCMArg any] saved:NO])) - .andCall(mockedBaParameterImpl, @selector(setValue:forKey:saved:)); - OCMStub(ClassMethod([(Class)baParameterMock removeObjectForKey:[OCMArg any]])) - .andCall(mockedBaParameterImpl, @selector(removeObjectForKey:)); - - BatchUserProfile *profile = [Batch defaultUserProfile]; - [profile setRegion:nil]; - [profile setLanguage:nil]; - [profile setCustomIdentifier:nil]; -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; - - [baParameterMock stopMocking]; -} - -- (void)testBasics { - BAUserProfile *profile = [BAUserProfile defaultUserProfile]; - XCTAssertNotNil(profile, @"Failed to get the default profile."); - - NSString *customIdentifier = [profile customIdentifier]; - XCTAssertNil(customIdentifier, @"Custom identifier not nil: %@.", customIdentifier); - - [profile setCustomIdentifier:@"batch.unit.tests"]; - customIdentifier = [profile customIdentifier]; - XCTAssertNotNil(customIdentifier, @"Failed to get the custom identifier."); - XCTAssertTrue([@"batch.unit.tests" isEqualToString:customIdentifier], @"Custom identifier not stored."); - - [profile setCustomIdentifier:nil]; - customIdentifier = [profile customIdentifier]; - XCTAssertNil(customIdentifier, @"Custom identifier not nil."); - - NSString *language = [profile language]; - XCTAssertNil(language, @"Default custom language value should be nil."); - - [profile setLanguage:@"batch.language"]; - language = [profile language]; - XCTAssertNotNil(language, @"Failed to get the language."); - XCTAssertTrue([@"batch.language" isEqualToString:language], @"Language not stored: %@.", language); - - [profile setLanguage:nil]; - language = [profile language]; - XCTAssertNil(language, @"Language not nil."); - - NSString *region = [profile language]; - XCTAssertNil(region, @"Default custom region value should be nil."); - - [profile setRegion:@"batch.region"]; - region = [profile region]; - XCTAssertNotNil(region, @"Failed to get the region."); - XCTAssertTrue([@"batch.region" isEqualToString:region], @"Region not stored: %@.", region); - - [profile setRegion:nil]; - region = [profile region]; - XCTAssertNil(region, @"Region not nil."); - - NSString *attributionID = [profile attributionID]; - XCTAssertNil(attributionID, @"Attribution identifier not nil: %@.", attributionID); - - [profile setAttributionID:@"batch.attribution.id"]; - attributionID = [profile attributionID]; - XCTAssertNotNil(attributionID, @"Failed to get the attribution identifier."); - XCTAssertTrue([@"batch.attribution.id" isEqualToString:attributionID], @"Attribution identifier not stored."); - - [profile setAttributionID:nil]; - attributionID = [profile attributionID]; - XCTAssertNil(attributionID, @"Attribution identifier not nil."); -} - -- (void)testVersion { - BatchUserProfile *profile = [Batch defaultUserProfile]; - XCTAssertNotNil(profile, @"Failed to get the default profile."); - - BAUserProfile *internalProfile = [BAUserProfile defaultUserProfile]; - XCTAssertNotNil(profile, @"Failed to get the internal profile."); - - NSNumber *firstVersion = [internalProfile version]; - NSNumber *version = [internalProfile version]; - XCTAssertNotNil(version, @"Profile version should not be nil."); - - // Test both change and lack of change for a value, and for nil, for every user profile parameter - - // Region - version = [internalProfile version]; - [profile setRegion:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setRegion:@"en"]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - version = [internalProfile version]; - [profile setRegion:nil]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setRegion:nil]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - version = [internalProfile version]; - [profile setRegion:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - // Language - version = [internalProfile version]; - [profile setLanguage:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setLanguage:@"en"]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - version = [internalProfile version]; - [profile setLanguage:nil]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setLanguage:nil]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - [profile setLanguage:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - // Custom ID - version = [internalProfile version]; - [profile setCustomIdentifier:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setCustomIdentifier:@"en"]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - version = [internalProfile version]; - [profile setCustomIdentifier:nil]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - version = [internalProfile version]; - [profile setCustomIdentifier:nil]; - XCTAssertEqualObjects(version, [internalProfile version], @"Profile version should not have changed"); - - [profile setCustomIdentifier:@"en"]; - [internalProfile incrementVersion]; - XCTAssertNotEqualObjects(version, [internalProfile version], @"Profile version should have changed"); - - XCTAssertGreaterThan([[internalProfile version] longLongValue], [firstVersion longLongValue], - @"Profile version should be greater than when we started"); -} - -#pragma clang diagnostic pop - -@end diff --git a/Sources/batchTests/Modules/Data Collection/dataCollectionCenterTests.swift b/Sources/batchTests/Modules/Data Collection/dataCollectionCenterTests.swift new file mode 100644 index 0000000..fdc35d5 --- /dev/null +++ b/Sources/batchTests/Modules/Data Collection/dataCollectionCenterTests.swift @@ -0,0 +1,119 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class dataCollectionCenterTests: XCTestCase { + // Mock event tracker + let eventTracker = MockEventTracker() + + // Data Collection Module + let dataCollectionCenter = BATDataCollectionCenter.sharedInstance + + override func setUp() { + super.setUp() + continueAfterFailure = false + + // Remove local config + BAParameter.removeObject(forKey: kParametersDataCollectionConfigKey) + + // Register event tracker overlay + let _ = eventTracker.registerOverlay() + eventTracker.reset() + + // Reset data collection config + dataCollectionCenter.setDefaultDataCollectionConfig() + } + + func testSystemParametersMayHaveChanged() throws { + // Remove initial data if necessary + BAParameter.removeObject(forKey: SystemParameterRegistry.deviceLanguage.userDefaultKey) + BAParameter.removeObject(forKey: SystemParameterRegistry.deviceRegion.userDefaultKey) + + // Start module + dataCollectionCenter.systemParametersMayHaveChanged() + + // Ensure removed parameters in setup has changed + let event = eventTracker.findEvent(name: .nativeDataChanged, parameters: nil) + XCTAssertNotNil(event) + XCTAssertEqual(event?.parametersDictionary["device_language"] as? String, BAPropertiesCenter.value(forShortName: "dla")) + XCTAssertEqual(event?.parametersDictionary["device_region"] as? String, BAPropertiesCenter.value(forShortName: "dre")) + } + + public func testBuildIdsForQuery() { + var ids = dataCollectionCenter.buildIdsForQuery() + var dataCollectDict = ids["data_collection"] as! [String: Any] + + // Ensure device_model is not in ids since disabled by default + XCTAssertFalse(ids.keys.contains("dty")) + + // Ensure geoip is false by default + XCTAssertTrue(dataCollectDict.keys.contains("geoip")) + XCTAssertEqual(dataCollectDict["geoip"] as? Bool, false) + + // Enable all + BatchSDK.updateAutomaticDataCollection { config in + config.setGeoIPEnabled(true) + config.setDeviceModelEnabled(true) + } + ids = dataCollectionCenter.buildIdsForQuery() + dataCollectDict = ids["data_collection"] as! [String: Any] + + // Ensure device_model is in ids since we enabled it + XCTAssertTrue(ids.keys.contains("dty")) + + // Ensure geoip is now true + XCTAssertTrue(dataCollectDict.keys.contains("geoip")) + XCTAssertEqual(dataCollectDict["geoip"] as? Bool, true) + } + + func testUpdateAutomaticDataCollection() throws { + // Enable all + BatchSDK.updateAutomaticDataCollection { config in + config.setGeoIPEnabled(true) + config.setDeviceModelEnabled(true) + } + + // Verify all data are sent + XCTAssertNotNil(eventTracker.findEvent(name: .nativeDataChanged, parameters: [ + "geoip_resolution": true, + "device_model": "Simulator - arm64", + ])) + + // Disable only geoip + BatchSDK.updateAutomaticDataCollection { config in + config.setGeoIPEnabled(false) + } + + // Check geoip is sent + XCTAssertNotNil(eventTracker.findEvent(name: .nativeDataChanged, parameters: [ + "geoip_resolution": false, + ])) + + // Disable only device model + BatchSDK.updateAutomaticDataCollection { config in + config.setDeviceModelEnabled(false) + } + + // Check device model is sent with null + XCTAssertNotNil(eventTracker.findEvent(name: .nativeDataChanged, parameters: [ + "device_model": NSNull(), + ])) + + eventTracker.reset() + + // Disable all + BatchSDK.updateAutomaticDataCollection { config in + config.setGeoIPEnabled(false) + config.setDeviceModelEnabled(false) + } + + // Ensure no event is sent since we already disabled it before + XCTAssertNil(eventTracker.findEvent(name: .nativeDataChanged, parameters: nil)) + } +} diff --git a/Sources/batchTests/Modules/Inbox/inboxFetcherTests.swift b/Sources/batchTests/Modules/Inbox/inboxFetcherTests.swift index 2b7ef4e..55531ac 100644 --- a/Sources/batchTests/Modules/Inbox/inboxFetcherTests.swift +++ b/Sources/batchTests/Modules/Inbox/inboxFetcherTests.swift @@ -1,5 +1,5 @@ // -// batchEventDataTests.swift +// inboxFetcherTests.swift // Batch // // Copyright © Batch.com. All rights reserved. diff --git a/Sources/batchTests/Modules/Inbox/inboxNotificationContentTests.m b/Sources/batchTests/Modules/Inbox/inboxNotificationContentTests.m index 72ec026..c1e7d3c 100644 --- a/Sources/batchTests/Modules/Inbox/inboxNotificationContentTests.m +++ b/Sources/batchTests/Modules/Inbox/inboxNotificationContentTests.m @@ -42,16 +42,13 @@ - (void)testNotificationContentValidPayload { failOnSilentNotification:true]; XCTAssertEqual(@"test-id", [content identifier]); - XCTAssertEqual(@"Je suis un title", [content title]); - XCTAssertEqual(@"Je suis un body", [content body]); XCTAssertEqual(@"Je suis un title", [[content message] title]); XCTAssertEqual(@"Je suis un body", [[content message] body]); XCTAssertNil([[content message] subtitle]); XCTAssertTrue([content isUnread]); - XCTAssertFalse([content isDeleted]); XCTAssertFalse([content isSilent]); XCTAssertEqual(BatchNotificationSourceTransactional, [content source]); - XCTAssertEqual(@"https://batch.com", [[content attachmentURL] absoluteString]); + XCTAssertEqualObjects(@"https://batch.com", [[content attachmentURL] absoluteString]); [content _markAsRead]; XCTAssertFalse([content isUnread]); @@ -64,8 +61,6 @@ - (void)testNotificationContentValidPayload { isUnread:TRUE date:now failOnSilentNotification:true]; - XCTAssertEqual(@"Je suis un title", [content title]); - XCTAssertEqual(@"Je suis un body", [content body]); XCTAssertEqual(@"Je suis un title", [[content message] title]); XCTAssertEqual(@"Je suis un body", [[content message] body]); XCTAssertEqual(@"Je suis un subtitle", [[content message] subtitle]); @@ -79,13 +74,10 @@ - (void)testNotificationContentValidPayload { date:now failOnSilentNotification:true]; XCTAssertEqual(@"test-id", [content identifier]); - XCTAssertNil([content title]); XCTAssertNil([[content message] title]); XCTAssertNil([[content message] subtitle]); - XCTAssertEqual(@"Je suis une alerte", [content body]); XCTAssertEqual(@"Je suis une alerte", [[content message] body]); XCTAssertTrue([content isUnread]); - XCTAssertFalse([content isDeleted]); XCTAssertFalse([content isSilent]); XCTAssertEqual(BatchNotificationSourceTransactional, [content source]); } @@ -173,10 +165,8 @@ - (void)testSilentNotifications { isUnread:TRUE date:now failOnSilentNotification:false]; - XCTAssertNil(content.title); XCTAssertNil(content.message); - XCTAssertEqual(@"", content.body); // Test compatibility behaviour - XCTAssertEqual(silentNotificationPayload, content.payload); + XCTAssertEqualObjects(silentNotificationPayload, content.payload); XCTAssertTrue(content.isSilent); } diff --git a/Sources/batchTests/Modules/Messaging/Webview/webviewJavascriptBridgeTests.swift b/Sources/batchTests/Modules/Messaging/Webview/webviewJavascriptBridgeTests.swift index 9bd98bf..be68ad7 100644 --- a/Sources/batchTests/Modules/Messaging/Webview/webviewJavascriptBridgeTests.swift +++ b/Sources/batchTests/Modules/Messaging/Webview/webviewJavascriptBridgeTests.swift @@ -48,10 +48,6 @@ class webviewJavascriptBridgeTests: XCTestCase { XCTAssertResolves(nil, executeDataMethod("getCustomRegion")) XCTAssertResolves(nil, executeDataMethod("getCustomUserID")) - XCTAssertResolves(BridgeExpectations.attributionID as NSString, executeDataMethod("getAttributionID")) - bridge.shouldReturnAttributionID = false - XCTAssertRejects(executeDataMethod("getAttributionID")) - XCTAssertNoPendingPromise(promises) } @@ -354,8 +350,7 @@ fileprivate enum BridgeExpectations { } fileprivate class MockBridgeDelegate: Mock, BATWebviewJavascriptBridgeDelegate { - func bridge(_ bridge: BATWebviewJavascriptBridge, shouldDismissMessageWithAnalyticsID analyticsIdentifier: String?) - { + func bridge(_ bridge: BATWebviewJavascriptBridge, shouldDismissMessageWithAnalyticsID analyticsIdentifier: String?) { super.call(bridge, analyticsIdentifier) } @@ -376,7 +371,6 @@ fileprivate class MockBridgeDelegate: Mock, BATWebviewJavascriptBridgeDelegate { fileprivate class MockBridge: BATWebviewJavascriptBridge { var shouldReturnCustomDatas = true - var shouldReturnAttributionID = true init(trackingID: String? = nil, delegate: BATWebviewJavascriptBridgeDelegate? = nil) { let message = BAMSGMessageWebView() @@ -388,13 +382,6 @@ fileprivate class MockBridge: BATWebviewJavascriptBridge { return BAPromise.resolved(BridgeExpectations.installationID) } - override func readAttributionIDFromSDK() -> String? { - if shouldReturnAttributionID { - return BridgeExpectations.attributionID - } - return nil - } - override func customRegion() -> BAPromise { if !shouldReturnCustomDatas { return BAPromise.resolved(nil) diff --git a/Sources/batchTests/Modules/Messaging/Webview/webviewTestHelpers.swift b/Sources/batchTests/Modules/Messaging/Webview/webviewTestHelpers.swift index 82c19b7..f8a84c8 100644 --- a/Sources/batchTests/Modules/Messaging/Webview/webviewTestHelpers.swift +++ b/Sources/batchTests/Modules/Messaging/Webview/webviewTestHelpers.swift @@ -24,7 +24,7 @@ class MinimalWebviewJavascriptBridge: BATWebviewJavascriptBridge { return BAPromise.rejected( NSError(domain: "tests", code: 0, userInfo: [NSLocalizedDescriptionKey: expectedErrorMessage])) case "echo": - return BAPromise.resolved(rawJSONArguments!["value"] as! NSString) + return BAPromise.resolved((rawJSONArguments!["value"] as! NSString)) case "echo_nil": return BAPromise.resolved(nil) default: diff --git a/Sources/batchTests/Modules/Profile/TestProfileCenter.swift b/Sources/batchTests/Modules/Profile/TestProfileCenter.swift new file mode 100644 index 0000000..3609f8b --- /dev/null +++ b/Sources/batchTests/Modules/Profile/TestProfileCenter.swift @@ -0,0 +1,30 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch +@testable import Batch.Batch_Private +import Foundation + +/// A test BATProfileEditor that has a controllable canSetEmail +class TestProfileCenter: BAProfileCenterProtocol { + public var onProjectChangedHasBeenCalled = false + + func identify(_: String?) {} + + func trackPublicEvent(name _: String, attributes _: BatchEventAttributes?) throws {} + + func trackLocation(_: CLLocation) {} + + func validateEventAttributes(_: BatchEventAttributes) -> [String] { + return [] + } + + func applyEditor(_: BATProfileEditor) {} + + func onProjectChanged(oldProjectKey _: String?, newProjectKey _: String?) { + onProjectChangedHasBeenCalled = true + } +} diff --git a/Sources/batchTests/Modules/Profile/TestProfileEditor.swift b/Sources/batchTests/Modules/Profile/TestProfileEditor.swift new file mode 100644 index 0000000..97f215d --- /dev/null +++ b/Sources/batchTests/Modules/Profile/TestProfileEditor.swift @@ -0,0 +1,18 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch +@testable import Batch.Batch_Private +import Foundation + +/// A test BATProfileEditor that has a controllable canSetEmail +class TestProfileEditor: BATProfileEditor { + public var test_canSetEmail = true + + override func canSetEmail() -> Bool { + return test_canSetEmail + } +} diff --git a/Sources/batchTests/Modules/Profile/eventDataSerializerTests.swift b/Sources/batchTests/Modules/Profile/eventDataSerializerTests.swift new file mode 100644 index 0000000..3c13212 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/eventDataSerializerTests.swift @@ -0,0 +1,92 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class eventDataSerializerTests: XCTestCase { + func testAttributesSerialization() throws { + // Tags are not tested here: the SDK can randomize the order, making it hard to compare in bulk + + let eventAttributes = BatchEventAttributes { a in + a.put("test_label", forKey: "$label") + a.put(self.makeCar(brand: "toyota"), forKey: "my_car") + a.put("a_test_string", forKey: "string_attr") + a.put(13, forKey: "int_attr") + a.put(13.4567, forKey: "double_attr") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "date_attr") + a.put(URL(string: "https://batch.com/")!, forKey: "url_attr") + a.put(["A", "B", "C"], forKey: "string_list") + a.put([self.makeCar(brand: "peugeot"), self.makeCar(brand: "audi")], forKey: "list_items") + } + + let json = try BATEventAttributesSerializer.serialize(eventAttributes: eventAttributes) + let expected: [AnyHashable: Any] = [ + "label": "test_label", + + "attributes": [ + "string_attr.s": "a_test_string", + "int_attr.i": 13, + "double_attr.f": 13.4567, + "date_attr.t": 1_596_975_143_000, + "url_attr.u": "https://batch.com/", + "string_list.a": ["A", "B", "C"], + "my_car.o": makeExpectedCar(brand: "toyota"), + "list_items.a": [makeExpectedCar(brand: "peugeot"), makeExpectedCar(brand: "audi")], + ], + ] + + XCTAssertEqual(expected as NSDictionary, json as NSDictionary) + } + + func testTagsSerialization() throws { + let eventAttributes = BatchEventAttributes { a in + a.put(["tagA", "tagB", "tagC", "tagC"], forKey: "$tags") + } + + let json = try BATEventAttributesSerializer.serialize(eventAttributes: eventAttributes) + + guard let tags = json["tags"] as? [String] else { + XCTFail("tags is missing from serialized attributes or not a string array") + return + } + XCTAssertEqual(3, tags.count) + XCTAssertTrue(tags.contains("taga")) + XCTAssertTrue(tags.contains("tagb")) + XCTAssertTrue(tags.contains("tagc")) + } + + func makeCar(brand: String) -> BatchEventAttributes { + return BatchEventAttributes { a in + a.put(brand, forKey: "brand") + a.put(2024, forKey: "year") + a.put(false, forKey: "4x4") + a.put(URL(string: "https://batch.com/")!, forKey: "model_url") + a.put(BatchEventAttributes { a in + a.put("manu", forKey: "manufacturer") + a.put(6, forKey: "cylinders") + a.put(3.5, forKey: "cylinder_capacity") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "manufacturing_date") + }, forKey: "engine") + } + } + + func makeExpectedCar(brand: String) -> [AnyHashable: Any] { + return [ + "brand.s": brand, + "year.i": 2024, + "model_url.u": "https://batch.com/", + "4x4.b": false, + "engine.o": [ + "manufacturer.s": "manu", + "cylinders.i": 6, + "cylinder_capacity.f": 3.5, + "manufacturing_date.t": 1_596_975_143_000, + ], + ] + } +} diff --git a/Sources/batchTests/Modules/Profile/eventDataValidatorTests.swift b/Sources/batchTests/Modules/Profile/eventDataValidatorTests.swift new file mode 100644 index 0000000..d3d4610 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/eventDataValidatorTests.swift @@ -0,0 +1,212 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class eventDataValidatorTests: XCTestCase { + func testEventNameValidity() throws { + let invalidEventNames = [ + "invalid event name", + "invalid-event-name", + "invalid_event_name@", + "invalid_event_name\n", + ] + for name in invalidEventNames { + XCTAssertThrowsError(try BAProfileCenter().trackPublicEvent(name: name, attributes: nil)) + } + + XCTAssertNoThrow(try BAProfileCenter().trackPublicEvent(name: "valid_event_name", attributes: nil)) + } + + func testLabel() throws { + let longLabel = String(repeating: "a_way_too_long_label", count: 20) + expectEventValidationError("$label: cannot be longer than 200 characters", attributes: BatchEventAttributes { a in + a.put(longLabel, forKey: "$label") + }) + + expectEventValidationError("$label: cannot be empty or only made of whitespace", attributes: BatchEventAttributes { a in + a.put("", forKey: "$label") + }) + + expectEventValidationError("$label: cannot be multiline", attributes: BatchEventAttributes { a in + a.put("with_multi_\n_line", forKey: "$label") + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put("a_valid_label", forKey: "$label") + }) + } + + func testTagsAttribute() throws { + expectEventValidationError("$tags: must not contain more than 10 values", attributes: BatchEventAttributes { a in + let tags = (0 ... 10).map { _ in "tag_$i" } + a.put(tags, forKey: "$tags") + }) + + expectEventValidationError("$tags[2]: tag cannot be empty or made of whitespace", attributes: BatchEventAttributes { a in + a.put(["tag_0", "tag_1", ""], forKey: "$tags") + }) + + expectEventValidationError("$tags[1]: tag cannot be longer than 64 characters", attributes: BatchEventAttributes { a in + a.put(["tag_0", String(repeating: "tag_", count: 20)], forKey: "$tags") + }) + + expectEventValidationError("$tags[2]: tag cannot be multiline", attributes: BatchEventAttributes { a in + a.put(["tag_0", "tag_1", "tag_\n_2"], forKey: "$tags") + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put(["tag_0", "tag_1", "tag_2"], forKey: "$tags") + }) + } + + func testStringAttribute() throws { + expectEventValidationError("string_attr: string attribute cannot be longer than 200 characters", attributes: BatchEventAttributes { a in + a.put(String(repeating: "too_long_", count: 100), forKey: "string_attr") + }) + + expectEventValidationError("string_attr: string attribute cannot be empty or made of whitespace", attributes: BatchEventAttributes { a in + a.put("", forKey: "string_attr") + }) + + expectEventValidationError("string_attr: string attribute cannot be multiline", attributes: BatchEventAttributes { a in + a.put("with_multi_\n_line", forKey: "string_attr") + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put("a_valid_string", forKey: "string_attr") + }) + } + + func testURLAttribute() throws { + expectEventValidationError("url_attr: URL attributes cannot be longer than 2048 characters", attributes: BatchEventAttributes { a in + a.put(URL(string: "https://batch.com/home?id=" + String(repeating: "too_long", count: 1000))!, forKey: "url_attr") + }) + + expectEventValidationError("url_attr: URL attributes must follow the format 'scheme://[authority][path][?query][#fragment]'", attributes: BatchEventAttributes { a in + a.put(URL(string: "batch.com")!, forKey: "url_attr") + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put(URL(string: "https://batch.com/home?id=123")!, forKey: "string_attr") + }) + } + + func testObjectAttribute() throws { + let errors = BATEventAttributesValidator(eventAttributes: BatchEventAttributes { a in + a.put(BatchEventAttributes { a in + a.put("a_valid_label", forKey: "$label") + a.put(["tag_0", "tag_1", "tag_2"], forKey: "$tags") + a.put(BatchEventAttributes { a in + a.put(BatchEventAttributes { a in + a.put(BatchEventAttributes { _ in }, forKey: "sub_obj_3") + }, + forKey: "sub_obj_2") + }, forKey: "sub_obj_1") + }, forKey: "obj_attr") + }).computeValidationErrors() + XCTAssertEqual(errors[0], "obj_attr.$label: Labels are not allowed in sub-objects") + XCTAssertEqual(errors[1], "obj_attr.$tags: Tags are not allowed in sub-objects") + XCTAssertEqual(errors[2], "obj_attr.sub_obj_1.sub_obj_2.sub_obj_3: Object attributes cannot be nested in more than three levels") + + expectEventValidationError(": objects cannot hold more than 20 attributes", attributes: BatchEventAttributes { a in + for i in 0 ... 20 { + a.put("val", forKey: "attr_\(i)") + } + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put(BatchEventAttributes { a in + a.put("car_brand", forKey: "brand") + a.put(2024, forKey: "year") + a.put(false, forKey: "4x4") + a.put(URL(string: "https://batch.com/")!, forKey: "model_url") + a.put(BatchEventAttributes { a in + a.put("manu", forKey: "manufacturer") + a.put(6, forKey: "cylinders") + a.put(3.5, forKey: "cylinder_capacity") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "manufacturing_date") + }, forKey: "engine") + }, forKey: "my_car") + a.put("a_test_string", forKey: "string_attr") + a.put(13, forKey: "int_attr") + a.put(13.4567, forKey: "double_attr") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "date_attr") + a.put(URL(string: "https://batch.com/")!, forKey: "url_attr") + a.put(["A", "B", "C"], forKey: "string_list") + a.put([ + BatchEventAttributes { a in + a.put("car_brand", forKey: "brand") + a.put(2024, forKey: "year") + a.put(false, forKey: "4x4") + a.put(URL(string: "https://batch.com/")!, forKey: "model_url") + a.put(BatchEventAttributes { a in + a.put("manu", forKey: "manufacturer") + a.put(6, forKey: "cylinders") + a.put(3.5, forKey: "cylinder_capacity") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "manufacturing_date") + }, forKey: "engine") + }, + BatchEventAttributes { a in + a.put("car_brand", forKey: "brand") + a.put(2024, forKey: "year") + a.put(false, forKey: "4x4") + a.put(URL(string: "https://batch.com/")!, forKey: "model_url") + a.put(BatchEventAttributes { a in + a.put("manu", forKey: "manufacturer") + a.put(6, forKey: "cylinders") + a.put(3.5, forKey: "cylinder_capacity") + a.put(Date(timeIntervalSince1970: 1_596_975_143), forKey: "manufacturing_date") + }, forKey: "engine") + }, + ], forKey: "list_items") + a.put("test_label", forKey: "$label") + a.put(["tagA", "tagB", "tagC", "tagC"], forKey: "$tags") + }) + } + + func testStringArrayAttribute() throws { + expectEventValidationError("string_array_attr[2]: string attribute cannot be longer than 200 characters", attributes: BatchEventAttributes { a in + a.put(["a", "b", String(repeating: "too_long", count: 40)], forKey: "string_array_attr") + }) + + expectEventValidationError("string_array_attr[1]: string attribute cannot be empty or made of whitespace", attributes: BatchEventAttributes { a in + a.put(["a", "", "c"], forKey: "string_array_attr") + }) + + expectEventValidationError("string_array_attr[1]: string attribute cannot be multiline", attributes: BatchEventAttributes { a in + a.put(["a", "with\nlinebreak", "c"], forKey: "string_array_attr") + }) + + expectEventValidationError("list_attr: array attributes cannot have more than 25 elements", attributes: BatchEventAttributes { a in + let array = (0 ... 25).map { _ in "val_$i" } + a.put(array, forKey: "list_attr") + }) + + expectEventValidationSuccess(attributes: BatchEventAttributes { a in + a.put(["a", "b", "c"], forKey: "string_array_attr") + }) + } + + func expectEventValidationSuccess(attributes: BatchEventAttributes, file: StaticString = #filePath, line: UInt = #line) { + let errors = BATEventAttributesValidator(eventAttributes: attributes).computeValidationErrors() + if !errors.isEmpty { + print("Expected no error, got: \(errors)") + } + XCTAssertTrue(errors.isEmpty, file: file, line: line) + } + + func expectEventValidationError(_ error: String, attributes: BatchEventAttributes, file: StaticString = #filePath, line: UInt = #line) { + let errors = BATEventAttributesValidator(eventAttributes: attributes).computeValidationErrors() + let foundError = errors.contains { $0.lowercased() == error.lowercased() } + if !foundError { + print("Expected '\(error)', Got: \(errors)") + } + XCTAssertTrue(foundError, file: file, line: line) + } +} diff --git a/Sources/batchTests/Modules/Profile/profileEditorCompatibilityTest.m b/Sources/batchTests/Modules/Profile/profileEditorCompatibilityTest.m new file mode 100644 index 0000000..e8ebcc8 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/profileEditorCompatibilityTest.m @@ -0,0 +1,72 @@ +// +// BatchTests +// +// Copyright © Batch. All rights reserved. +// + +#import +#import "BAInstallDataEditor.h" +#import "BatchProfile.h" + +#import "OCMock.h" + +@interface profileEditorCompatibilityTest : XCTestCase { +} +@end + +@implementation profileEditorCompatibilityTest + +- (void)testProfileEditorInstallCompatibility { + id installDataEditorMock = OCMClassMock([BAInstallDataEditor class]); + [installDataEditorMock setExpectationOrderMatters:YES]; + + [BAInjection overlayClass:BAInstallDataEditor.class returnedInstance:installDataEditorMock]; + + NSDate *testDate = [NSDate now]; + NSURL *testURL = [NSURL URLWithString:@"https://batch.com"]; + + OCMExpect([installDataEditorMock setLanguage:@"fr"]); + OCMExpect([installDataEditorMock setRegion:@"US"]); + OCMExpect([installDataEditorMock removeAttributeForKey:@"remove"]); + OCMExpect([installDataEditorMock clearTagCollection:@"remove"]); + OCMExpect([installDataEditorMock setBooleanAttribute:true forKey:@"bool" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setDateAttribute:testDate forKey:@"date" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setStringAttribute:@"foo" forKey:@"str" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setLongLongAttribute:23 forKey:@"int" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setLongLongAttribute:24 forKey:@"intl" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setDoubleAttribute:25 forKey:@"float" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setDoubleAttribute:26 forKey:@"dbl" error:[OCMArg anyObjectRef]]); + OCMExpect([installDataEditorMock setURLAttribute:testURL forKey:@"url" error:[OCMArg anyObjectRef]]); + + OCMExpect([installDataEditorMock clearTagCollection:@"strarray"]); + OCMExpect([installDataEditorMock addTag:@"foo" inCollection:@"strarray"]); + OCMExpect([installDataEditorMock addTag:@"bar" inCollection:@"strarray"]); + OCMExpect([installDataEditorMock addTag:@"baz" inCollection:@"strarray2"]); + OCMExpect([(BAInstallDataEditor *)installDataEditorMock removeTag:@"zab" fromCollection:@"strarray3"]); + + OCMExpect([installDataEditorMock save]); + + [BatchProfile editWithBlock:^(BatchProfileEditor *_Nonnull editor) { + [editor setLanguage:@"fr" error:nil]; + [editor setRegion:@"US" error:nil]; + [editor removeAttributeForKey:@"remove" error:nil]; + [editor setBooleanAttribute:true forKey:@"bool" error:nil]; + [editor setDateAttribute:testDate forKey:@"date" error:nil]; + [editor setStringAttribute:@"foo" forKey:@"str" error:nil]; + [editor setIntegerAttribute:23 forKey:@"int" error:nil]; + [editor setLongLongAttribute:24 forKey:@"intl" error:nil]; + // We can't easily test primitive float values using OCMock, the matching + // will randomly fail and we can't make a custom matcher + [editor setFloatAttribute:25 forKey:@"float" error:nil]; + [editor setDoubleAttribute:26 forKey:@"dbl" error:nil]; + [editor setURLAttribute:testURL forKey:@"url" error:nil]; + + [editor setStringArrayAttribute:@[ @"foo", @"bar" ] forKey:@"strarray" error:nil]; + [editor addItemToStringArrayAttribute:@"baz" forKey:@"strarray2" error:nil]; + [editor removeItemFromStringArrayAttribute:@"zab" forKey:@"strarray3" error:nil]; + }]; + + OCMVerifyAll(installDataEditorMock); +} + +@end diff --git a/Sources/batchTests/Modules/Profile/profileEditorValidationTests.swift b/Sources/batchTests/Modules/Profile/profileEditorValidationTests.swift new file mode 100644 index 0000000..1e07f89 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/profileEditorValidationTests.swift @@ -0,0 +1,49 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class profileEditorValidationTests: XCTestCase { + func testEmailPatterns() { + XCTAssertTrue(BATProfileDataValidators.isValidEmail("foo@batch.com")) + XCTAssertTrue(BATProfileDataValidators.isValidEmail("bar@foo.batch.com")) + XCTAssertTrue(BATProfileDataValidators.isValidEmail("bar+foo@batch.com")) + XCTAssertTrue(BATProfileDataValidators.isValidEmail("FOObar@Test.Batch.COM")) + + XCTAssertFalse(BATProfileDataValidators.isValidEmail("@gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("invalid@gmail")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("invalid@gmail .com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("in valid@gmail .com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("invalid@inva lid.gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("inva\nlid@invalid.gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("in+va\nlid@invalid.gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("invalid@inv\nalid.gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("invalid@invalid.gmail.com\n")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("\ninvalid@invalid.gmail.com")) + XCTAssertFalse(BATProfileDataValidators.isValidEmail("inval\rid@invalid.gmail.com\n")) + } + + func testEmailIsTooLong() { + XCTAssertTrue(BATProfileDataValidators.isEmailTooLong("testastringtestastringtoolongtobeanemailtestastringtoolongtestastringtoolongtobeanemailtestastringtoolongtestastringtoolongtobeanemailtestastringtoolongtestastringtoolongtobeanemailtestastringtoolongtestastringtoolongtobeanemailtestastringtoolongtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoolongtobeanemailtestastringtoo@batch.com")) + XCTAssertFalse(BATProfileDataValidators.isEmailTooLong("bar@foo.batch.com")) + } + + func testEditorEmailErrors() { + let editor = TestProfileEditor() + editor.test_canSetEmail = true + XCTAssertNoThrow(try editor.setEmail("test@batch.com")) + XCTAssertThrowsError(try editor.setEmail("invalid@inva lid.gmail.com")) + let longEmailPart = String(repeating: "test_too_long", count: 100) + XCTAssertThrowsError(try editor.setEmail("\(longEmailPart)@batch.com)")) + } + + func testCustomUserIDValidity() { + XCTAssertFalse(BATProfileDataValidators.isCustomIDTooLong("customId")) + XCTAssertTrue(BATProfileDataValidators.isCustomIDTooLong("my_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_idmy_test_id_1111")) + } +} diff --git a/Sources/batchTests/Modules/Profile/profileIdentifyTests.swift b/Sources/batchTests/Modules/Profile/profileIdentifyTests.swift new file mode 100644 index 0000000..284e987 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/profileIdentifyTests.swift @@ -0,0 +1,97 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class profileIdentifyTests: XCTestCase { + // Test that a valid custom ID triggers an identify + func testValidIdentification() throws { + let expectedCustomID = "identifier12345678" + + let eventTracker = MockEventTracker() + let _ = eventTracker.registerOverlay() + // Reinstanciate it as the shared instance did not get our event tracker mock + let profileModule = BAProfileCenter() + + profileModule.identify(expectedCustomID) + + XCTAssertNotNil(eventTracker.findEvent(name: .profileIdentify, parameters: [ + "identifiers": [ + "custom_id": expectedCustomID, + "install_id": BatchUser.installationID!, + ], + ])) + + // Logout + profileModule.identify(nil) + + XCTAssertNotNil(eventTracker.findEvent(name: .profileIdentify, parameters: [ + "identifiers": [ + "custom_id": NSNull(), + "install_id": BatchUser.installationID!, + ], + ])) + } + + // Test that an invalid custom id doesn't do anything + func testInvalidIdentification() throws { + let invalidCustomIDs: [String] = [ + String(repeating: "foo", count: 1000), + "", + "foo\nbar", + ] + + let eventTracker = MockEventTracker() + let _ = eventTracker.registerOverlay() + let profileModule = BAProfileCenter() + + for customID in invalidCustomIDs { + print("Testing identify: '\(customID)'") + profileModule.identify(customID) + + XCTAssertNil(eventTracker.findEvent(name: .profileIdentify, parameters: [ + "identifiers": [ + "custom_id": customID, + "install_id": BatchUser.installationID!, + ], + ])) + } + } + + // Test that setting a custom ID calls the compatiblity code + func testCompatibility() throws { + let expectedCustomID = "identifier12345678" + + let eventTracker = MockEventTracker() + let _ = eventTracker.registerOverlay() + let profileModule = BAProfileCenter() + + let installDataEditor = BAInstallDataEditorMock() + let _ = BAInjection.overlayClass(BAInstallDataEditor.self, returnedInstance: installDataEditor) + + profileModule.identify(expectedCustomID) + + XCTAssertEqual(installDataEditor.identifierToSet, expectedCustomID) + XCTAssertTrue(installDataEditor.saveCalled) + + // Logout + installDataEditor.reset() + profileModule.identify(nil) + + XCTAssertTrue(installDataEditor.setIdentifierCalled) + XCTAssertEqual(installDataEditor.identifierToSet, nil) + XCTAssertTrue(installDataEditor.saveCalled) + + // Invalid + installDataEditor.reset() + let invalidCustomID = String(repeating: "foo", count: 1000) + profileModule.identify(invalidCustomID) + XCTAssertFalse(installDataEditor.setIdentifierCalled) + // "save" can be called or not on an invalid custom id, we don't care + } +} diff --git a/Sources/batchTests/Modules/Profile/profileMigrationTests.swift b/Sources/batchTests/Modules/Profile/profileMigrationTests.swift new file mode 100644 index 0000000..ca110a4 --- /dev/null +++ b/Sources/batchTests/Modules/Profile/profileMigrationTests.swift @@ -0,0 +1,139 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +class profileMigrationTests: XCTestCase { + // Mock event tracker + let eventTracker = MockEventTracker() + + override func setUp() { + super.setUp() + continueAfterFailure = false + + // Remove project key + BAParameter.removeObject(forKey: kParametersProjectKey) + + // Register event tracker overlay + let _ = eventTracker.registerOverlay() + } + + override func tearDown() { + // reset event tracker + eventTracker.reset() + } + + func testOnProjectChanged() throws { + let profileCenterMock = TestProfileCenter() + BAInjection.overlayProtocol(BAProfileCenterProtocol.self, returnedInstance: profileCenterMock) + XCTAssertFalse(profileCenterMock.onProjectChangedHasBeenCalled) + + triggerMigrationsFromUserDataService() + + XCTAssertTrue(profileCenterMock.onProjectChangedHasBeenCalled) + } + + func testCustomIDMigration() throws { + let expectedCustomID = "fake-test-custom-id" + + // Add custom id from compat + BAUserProfile.default().customIdentifier = expectedCustomID + + // trigger fake project changed + triggerMigrationsFromUserDataService() + + // Ensure identify event is sent + XCTAssertNotNil(eventTracker.findEvent(name: .profileIdentify, parameters: [ + "identifiers": [ + "custom_id": expectedCustomID, + "install_id": BatchUser.installationID!, + ], + ])) + } + + func testCustomIDMigrationDisabled() throws { + let expectedCustomID = "fake-test-custom-id" + + // Add custom id from compat + BAUserProfile.default().customIdentifier = expectedCustomID + + BatchSDK.setDisabledMigrations([.customID]) + + // trigger fake project changed + triggerMigrationsFromUserDataService() + + // Ensure identify event is sent + XCTAssertNil(eventTracker.findEvent(name: .profileIdentify, parameters: nil)) + } + + func testCustomDataMigration() throws { + // Add custom id from compat + BAUserProfile.default().language = "fr" + BAUserProfile.default().region = "FR" + + let datasource = BAUserSQLiteDatasource.instance() + // Clearing tables to avoid parasites data from other tests + datasource?.clear() + datasource?.acquireTransactionLock(withChangeset: 1) + datasource?.setStringAttribute("teststring", forKey: "string") + datasource?.setLongLongAttribute(3, forKey: "long") + datasource?.setURLAttribute(URL(string: "https://batch.com/pricing")!, forKey: "url") + datasource?.addTag("tag1", toCollection: "testco") + datasource?.commitTransaction() + + // Force real data source + BAInjection.overlayProtocol(BAUserDatasourceProtocol.self, returnedInstance: datasource) + + // trigger fake project changed + triggerMigrationsFromUserDataService() + + // waiting dispatch queue + waitForQueueLoop(queue: BAUserDataManager.sharedQueue()) + + let event = eventTracker.findEvent(name: .profileDataChanged, parameters: nil) + XCTAssertEqual(event?.parametersDictionary["language"] as? String, "fr") + XCTAssertEqual(event?.parametersDictionary["region"] as? String, "FR") + // TODO: fix this test on CI +// let customAttributes = event?.parametersDictionary["custom_attributes"] as? NSDictionary +// XCTAssertEqual(customAttributes?["string.s"] as? String, "teststring") +// XCTAssertEqual(customAttributes?["long.i"] as? Int64, 3) +// XCTAssertEqual(customAttributes?["url.u"] as? String, "https://batch.com/pricing") +// +// let tags = ["tag1"] +// XCTAssertEqual(customAttributes?["testco.a"] as? [String], tags) + } + + func testCustomDataMigrationDisabled() throws { + let eventTracker = MockEventTracker() + let _ = eventTracker.registerOverlay() + + // Add custom id from compat + BAUserProfile.default().language = "fr" + + BatchSDK.setDisabledMigrations([.customData]) + + // trigger fake project changed + triggerMigrationsFromUserDataService() + + // Ensure identify event is sent + XCTAssertNil(eventTracker.findEvent(name: .profileDataChanged, parameters: nil)) + } + + private func triggerMigrationsFromUserDataService() { + let fakeResponse: [AnyHashable: Any]! = [ + "id": "test-id", + "action": "ok", + "project_key": "project_12345678", + ] + let userDataService: BAQueryWebserviceClientDelegate = BAUserDataCheckServiceDelegate() + let queryResponse: BAWSResponse = BAWSResponseAttributesCheck(response: fakeResponse) + let datasource: BAQueryWebserviceClientDatasource = BAUserDataCheckServiceDatasource(version: 0, transactionID: "transac") + let client = BAQueryWebserviceClient(datasource: datasource, delegate: nil) + userDataService.webserviceClient(client, didSucceedWith: [queryResponse]) + } +} diff --git a/Sources/batchTests/Modules/Profile/profileOperationsSerializerTests.swift b/Sources/batchTests/Modules/Profile/profileOperationsSerializerTests.swift new file mode 100644 index 0000000..52576bb --- /dev/null +++ b/Sources/batchTests/Modules/Profile/profileOperationsSerializerTests.swift @@ -0,0 +1,136 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class profileOperationsSerializerTests: XCTestCase { + func testEmptySerialization() throws { + XCTAssertTrue(serializeEditor { _ in }.isEmpty) + } + + func testComplexSerialization() throws { + let dateTimestamp = 1_596_975_143 + + let serialized = try serializeEditor { editor in + try editor.setEmail("test@batch.com") + editor.setEmailMarketingSubscriptionState(.subscribed) + try editor.setLanguage("fr_ch") + try editor.setRegion("FR") + try editor.setCustom(stringAttribute: "hello", forKey: "string_att") + try editor.setCustom(int64Attribute: 3, forKey: "int_att") + try editor.setCustom(doubleAttribute: 3.68, forKey: "double_att") + try editor.setCustom(boolAttribute: true, forKey: "bool_att") + try editor.setCustom(urlAttribute: URL(string: "https://batch.com/")!, forKey: "url_att") + try editor.setCustom(dateAttribute: Date(timeIntervalSince1970: TimeInterval(dateTimestamp)) as NSDate, forKey: "date_att") + try editor.setCustom(stringArrayAttribute: ["foo", "bar", "foo"], forKey: "string_array_att") + + try editor.deleteCustomAttribute(forKey: "delete_att") + + try editor.setCustom(stringAttribute: "foo", forKey: "overwrite") + try editor.setCustom(int64Attribute: 5, forKey: "overwrite") + + try editor.setCustom(stringAttribute: "foo", forKey: "overwrite_array") + try editor.add(value: "foo", toArray: "overwrite_array") + + try editor.add(value: "foo", toArray: "append_array_att") + try editor.add(value: "bar", toArray: "append_array_att") + try editor.remove(value: "baz", fromArray: "remove_array_att") + + try editor.setCustom(stringArrayAttribute: ["foo", "bar"], forKey: "complex_string_array_att") + try editor.remove(value: "bar", fromArray: "complex_string_array_att") + try editor.remove(value: "bar2", fromArray: "complex_string_array_att") + try editor.add(value: "foo", toArray: "complex_string_array_att") + try editor.add(value: "baz", toArray: "complex_string_array_att") + try editor.add(value: "baz2", toArray: "complex_string_array_att") + try editor.remove(value: "baz", fromArray: "complex_string_array_att") + + // Making an array and removing all its values should make it deleted + try editor.setCustom(stringArrayAttribute: ["foo", "bar"], forKey: "absent_array_att") + try editor.remove(value: "foo", fromArray: "absent_array_att") + try editor.remove(value: "bar", fromArray: "absent_array_att") + } + + XCTAssertEqual(serialized["email"] as? String, "test@batch.com") + XCTAssertEqual(serialized["email_marketing"] as? String, "subscribed") + XCTAssertEqual(serialized["language"] as? String, "fr_ch") + XCTAssertEqual(serialized["region"] as? String, "FR") + + guard let serializedAttributes = serialized["custom_attributes"] as? [AnyHashable: Any] else { + XCTFail("missing 'custom_attributes'") + return + } + + XCTAssertEqual(serializedAttributes["string_att.s"] as? String, "hello") + XCTAssertEqual(serializedAttributes["int_att.i"] as? Int, 3) + XCTAssertEqual(serializedAttributes["double_att.f"] as? Double, 3.68) + XCTAssertEqual(serializedAttributes["url_att.u"] as? String, "https://batch.com/") + XCTAssertEqual(serializedAttributes["date_att.t"] as? Int, dateTimestamp * 1000) + XCTAssertEqual(serializedAttributes["string_array_att.a"] as? [String], ["foo", "bar", "foo"]) + XCTAssertEqual(serializedAttributes["delete_att"] as? NSObject, NSNull()) + + XCTAssertEqual(serializedAttributes["append_array_att.a"] as? NSDictionary, ["$add": ["foo", "bar"]] as NSDictionary) + XCTAssertEqual(serializedAttributes["remove_array_att.a"] as? NSDictionary, ["$remove": ["baz"]] as NSDictionary) + + XCTAssertEqual(serializedAttributes["complex_string_array_att.a"] as? [String], ["foo", "foo", "baz2"]) + + XCTAssertNil(serializedAttributes["absent_array_att.a"]) + } + + /// Test that overriding previously set attributes properly works + func testAttributeOverride() throws { + let serialized = try serializeEditor { editor in + try editor.setCustom(stringAttribute: "hello", forKey: "att1") + try editor.setCustom(int64Attribute: 4, forKey: "att1") + + try editor.setCustom(stringAttribute: "foo", forKey: "att2") + try editor.setCustom(stringArrayAttribute: ["bar"], forKey: "att2") + + try editor.setCustom(stringAttribute: "bar", forKey: "att3") + try editor.add(value: "baz", toArray: "att3") + + try editor.setCustom(stringAttribute: "bar", forKey: "att4") + try editor.remove(value: "baz", fromArray: "att4") + + try editor.add(value: "baz", toArray: "att5") + try editor.setCustom(int64Attribute: 5, forKey: "att5") + } + + XCTAssertNil(serialized["email"]) + XCTAssertNil(serialized["email_marketing"]) + XCTAssertNil(serialized["language"]) + XCTAssertNil(serialized["region"]) + guard let serializedAttributes = serialized["custom_attributes"] as? [AnyHashable: Any] else { + XCTFail("missing 'custom_attributes'") + return + } + + XCTAssertEqual(serializedAttributes["att1.i"] as? Int, 4) + XCTAssertEqual(serializedAttributes["att2.a"] as? [String], ["bar"]) + XCTAssertEqual(serializedAttributes["att3.a"] as? NSDictionary, ["$add": ["baz"]] as NSDictionary) + XCTAssertEqual(serializedAttributes["att4.a"] as? NSDictionary, ["$remove": ["baz"]] as NSDictionary) + XCTAssertEqual(serializedAttributes["att5.i"] as? Int, 5) + } + + /// Test that an email cannot be set and is not serialized if not allowed + func testCantSetEmail() throws { + let editor = TestProfileEditor() + editor.test_canSetEmail = false + + XCTAssertThrowsError(try editor.setEmail("test@batch.com")) + + let serialized = BATProfileOperationsSerializer.serialize(profileEditor: editor) + XCTAssertNil(serialized["email"]) + } + + func serializeEditor(_ editClosure: (BATProfileEditor) throws -> Void) rethrows -> [AnyHashable: Any] { + let editor = TestProfileEditor() + editor.test_canSetEmail = true + try editClosure(editor) + return BATProfileOperationsSerializer.serialize(profileEditor: editor) + } +} diff --git a/Sources/batchTests/Modules/Push/batchUNUserNotificationCenterDelegateTests.m b/Sources/batchTests/Modules/Push/batchUNUserNotificationCenterDelegateTests.m index 60e0a68..c49fa31 100644 --- a/Sources/batchTests/Modules/Push/batchUNUserNotificationCenterDelegateTests.m +++ b/Sources/batchTests/Modules/Push/batchUNUserNotificationCenterDelegateTests.m @@ -74,6 +74,7 @@ - (void)testForwarding { didReceiveNotificationResponse:notificationResponseMock]); BatchUNUserNotificationCenterDelegate *delegate = [BatchUNUserNotificationCenterDelegate new]; + delegate.showForegroundNotifications = false; __block BOOL completionHandlerCalled = false; @@ -106,23 +107,15 @@ - (void)testForegroundNotifications { // Test that the foreground option works in both modes OCMExpect([baPushCenterMock handleUserNotificationCenter:[OCMArg any] willPresentNotification:[OCMArg any] - willShowSystemForegroundAlert:false]); + willShowSystemForegroundAlert:true]); OCMExpect([baPushCenterMock handleUserNotificationCenter:[OCMArg any] willPresentNotification:[OCMArg any] - willShowSystemForegroundAlert:true]); + willShowSystemForegroundAlert:false]); BatchUNUserNotificationCenterDelegate *delegate = [BatchUNUserNotificationCenterDelegate new]; // Test default value - XCTAssertFalse(delegate.showForegroundNotifications); - - // Test that the delegate doesn't show foreground notifications - [delegate userNotificationCenter:unNotificationCenterMock - willPresentNotification:notificationMock - withCompletionHandler:^(UNNotificationPresentationOptions options) { - XCTAssertEqual(0, options); - }]; + XCTAssertTrue(delegate.showForegroundNotifications); - delegate.showForegroundNotifications = true; // Test that the delegate can show foreground notifications [delegate userNotificationCenter:unNotificationCenterMock willPresentNotification:notificationMock @@ -137,6 +130,14 @@ - (void)testForegroundNotifications { XCTAssertEqual(expectedOptions, options); }]; + // Test that the delegate doesn't show foreground notifications + delegate.showForegroundNotifications = false; + [delegate userNotificationCenter:unNotificationCenterMock + willPresentNotification:notificationMock + withCompletionHandler:^(UNNotificationPresentationOptions options) { + XCTAssertEqual(0, options); + }]; + OCMVerifyAll(baPushCenterMock); } diff --git a/Sources/batchTests/Modules/Push/pushAuthorizationTests.swift b/Sources/batchTests/Modules/Push/pushAuthorizationTests.swift new file mode 100644 index 0000000..f079c2d --- /dev/null +++ b/Sources/batchTests/Modules/Push/pushAuthorizationTests.swift @@ -0,0 +1,60 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class pushAuthorizationTests: XCTestCase { + func testRegister() async throws { + let pushSystemHelper = MockPushSystemHelper() + let _ = BAInjection.overlayProtocol(BAPushSystemHelperProtocol.self, returnedInstance: pushSystemHelper) + + var result = try await BatchPush.requestNotificationAuthorization() + XCTAssertTrue(result) + result = try await BatchPush.requestProvisionalNotificationAuthorization() + XCTAssertFalse(result) + } + + func testRegisterSync() { + let pushSystemHelper = MockPushSystemHelper() + let _ = BAInjection.overlayProtocol(BAPushSystemHelperProtocol.self, returnedInstance: pushSystemHelper) + + BatchPush.requestNotificationAuthorization() + XCTAssertTrue(pushSystemHelper.calledRegister) + XCTAssertFalse(pushSystemHelper.calledProvisionalRegister) + + pushSystemHelper.reset() + BatchPush.requestProvisionalNotificationAuthorization() + XCTAssertFalse(pushSystemHelper.calledRegister) + XCTAssertTrue(pushSystemHelper.calledProvisionalRegister) + } +} + +@objc +fileprivate class MockPushSystemHelper: NSObject, BAPushSystemHelperProtocol { + public var calledRegister = false + public var calledProvisionalRegister = false + + func register(forRemoteNotificationsTypes _: BatchNotificationType, providesNotificationSettings _: Bool, completionHandler: ((Bool, (any Error)?) -> Void)!) { + calledRegister = true + if let completionHandler { + completionHandler(true, nil) + } + } + + func register(forProvisionalNotifications _: BatchNotificationType, providesNotificationSettings _: Bool, completionHandler: ((Bool, (any Error)?) -> Void)!) { + calledProvisionalRegister = true + if let completionHandler { + completionHandler(false, nil) + } + } + + func reset() { + calledRegister = false + calledProvisionalRegister = false + } +} diff --git a/Sources/batchTests/Modules/Tracker/batchEventSQLiteDatasourceTests.m b/Sources/batchTests/Modules/Tracker/batchEventSQLiteDatasourceTests.m index 011897a..ecc945c 100644 --- a/Sources/batchTests/Modules/Tracker/batchEventSQLiteDatasourceTests.m +++ b/Sources/batchTests/Modules/Tracker/batchEventSQLiteDatasourceTests.m @@ -55,10 +55,7 @@ - (void)testInsert { NSArray *events = [_datasource eventsToSend:100]; XCTAssertTrue([events count] == 3, @"Event table shound contain 3 events"); - events = [_datasource eventsToSend:2]; - XCTAssertTrue([events count] == 2, @"Event table shound contain 2 events when limited to 2"); - - BAEvent *event = [events objectAtIndex:0]; + BAEvent *event = [events lastObject]; XCTAssertTrue([eventName2 isEqualToString:event.name], @"Event name badly persisted"); XCTAssertTrue(event.state == BAEventStateNew, @"Bad event state"); NSDictionary *parameters = [BAJson deserializeAsDictionary:event.parameters error:nil]; @@ -66,8 +63,12 @@ - (void)testInsert { XCTAssertTrue([eventValue isEqualToString:[parameters objectForKey:eventKey]], @"Event parameters wrongly deserialized"); - event = [events objectAtIndex:1]; + event = [events firstObject]; XCTAssertTrue([eventName isEqualToString:event.name], @"Event name badly persisted"); + + // Test limiting + events = [_datasource eventsToSend:2]; + XCTAssertTrue([events count] == 2, @"Event table shound contain 2 events when limited to 2"); } - (void)testCollapsableInsert { @@ -89,11 +90,11 @@ - (void)testCollapsableInsert { NSArray *events = [_datasource eventsToSend:100]; XCTAssertTrue([events count] == 2, @"Event table shound contain 2 collapsed events"); - BAEvent *event = [events objectAtIndex:0]; + BAEvent *event = [events lastObject]; XCTAssertTrue([eventName2 isEqualToString:event.name], @"Event name badly persisted"); XCTAssertTrue(event.state == BAEventStateNew, @"Bad event state"); - event = [events objectAtIndex:1]; + event = [events firstObject]; XCTAssertTrue([eventName isEqualToString:event.name], @"Event name badly persisted"); } @@ -132,4 +133,19 @@ - (void)testDelete { } } +/// Events should be returned in the same order as they're inserted +- (void)testOrder { + NSArray *eventNames = @[ @"_FIRST", @"E.SECOND", @"THIRD", @"_FOURTH" ]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wall" + for (NSString *name in eventNames) { + [_datasource addEvent:[BAEvent eventWithName:name]]; + } +#pragma clang diagnostic pop + NSArray *eventsToSend = [_datasource eventsToSend:4]; + for (int i = 0; i < eventsToSend.count; i++) { + XCTAssertEqualObjects(eventsToSend[i].name, eventNames[i]); + } +} + @end diff --git a/Sources/batchTests/Modules/User/batchUserEditorPublicAPITests.m b/Sources/batchTests/Modules/User/batchUserEditorPublicAPITests.m deleted file mode 100644 index d1d33e4..0000000 --- a/Sources/batchTests/Modules/User/batchUserEditorPublicAPITests.m +++ /dev/null @@ -1,93 +0,0 @@ -#import -@import Batch; -@import Batch.Batch_Private; -#import "OCMock.h" - -@interface batchUserEditorPublicAPITests : XCTestCase - -@end - -/// Tests BatchUserDataEditor's public API -@implementation batchUserEditorPublicAPITests - -- (void)testPublicMethods { - id mockBackingEditor = OCMClassMock(BAUserDataEditor.class); - __unused id overlay = [BAInjection overlayClass:BAUserDataEditor.class returnedInstance:mockBackingEditor]; - - NSError *fakeErr = nil; - NSDate *date = [NSDate date]; - NSURL *url = [NSURL URLWithString:@"https://batch.com"]; - // OCMock doesn't deal with NSError** well - // Unfortunately this doesn't test that the error is not nil, but - // I coudldn't manage to fix it. - NSError *__autoreleasing *anyError = [OCMArg anyObjectRef]; - - BatchUserDataEditor *editor = [BatchUser editor]; - [editor setLanguage:@"fr"]; - [editor setRegion:@"fr"]; - [editor setIdentifier:@"foo"]; - [editor setAttributionIdentifier:@"EA7583CD-A667-48BC-B806-42ECB2B48606"]; - [editor setEmail:@"test@batch.com" error:nil]; - [editor setEmailMarketingSubscriptionState:BatchEmailSubscriptionStateSubscribed]; - [editor setAttribute:@"foo" forKey:@"bar"]; - [editor setAttribute:nil forKey:@"bar"]; - [editor setBooleanAttribute:true forKey:@"bar" error:&fakeErr]; - [editor setDateAttribute:date forKey:@"bar" error:&fakeErr]; - [editor setStringAttribute:@"foo" forKey:@"bar" error:&fakeErr]; - [editor setNumberAttribute:@2 forKey:@"bar" error:&fakeErr]; - [editor setIntegerAttribute:2 forKey:@"bar" error:&fakeErr]; - [editor setLongLongAttribute:2 forKey:@"bar" error:&fakeErr]; - [editor setFloatAttribute:1.234F forKey:@"bar" error:&fakeErr]; - [editor setDoubleAttribute:1.234L forKey:@"bar" error:&fakeErr]; - [editor setURLAttribute:url forKey:@"bar" error:&fakeErr]; - [editor removeAttributeForKey:@"bar"]; - [editor clearAttributes]; - [editor addTag:@"foo" inCollection:@"bar"]; - [editor removeTag:@"foo" fromCollection:@"bar"]; - [editor clearTags]; - [editor clearTagCollection:@"bar"]; - [editor save]; - OCMVerify([mockBackingEditor setLanguage:@"fr"]); - OCMVerify([mockBackingEditor setRegion:@"fr"]); - OCMVerify([mockBackingEditor setIdentifier:@"foo"]); - OCMVerify([mockBackingEditor setAttributionIdentifier:@"EA7583CD-A667-48BC-B806-42ECB2B48606"]); - OCMVerify([mockBackingEditor setEmail:@"test@batch.com" error:nil]); - OCMVerify([mockBackingEditor setEmailMarketingSubscriptionState:BatchEmailSubscriptionStateSubscribed]); - OCMVerify([mockBackingEditor setAttribute:@"foo" forKey:@"bar"]); - OCMVerify([mockBackingEditor setAttribute:nil forKey:@"bar"]); - OCMVerify([mockBackingEditor setBooleanAttribute:true forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setDateAttribute:date forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setStringAttribute:@"foo" forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setNumberAttribute:@2 forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setIntegerAttribute:2 forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setLongLongAttribute:2 forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setFloatAttribute:1.234F forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setDoubleAttribute:1.234L forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor setURLAttribute:url forKey:@"bar" error:anyError]); - OCMVerify([mockBackingEditor removeAttributeForKey:@"bar"]); - OCMVerify([(BAUserDataEditor *)mockBackingEditor clearAttributes]); - OCMVerify([mockBackingEditor addTag:@"foo" inCollection:@"bar"]); - OCMVerify([(BAUserDataEditor *)mockBackingEditor removeTag:@"foo" fromCollection:@"bar"]); - OCMVerify([(BAUserDataEditor *)mockBackingEditor clearTags]); - OCMVerify([mockBackingEditor clearTagCollection:@"bar"]); - OCMVerify([mockBackingEditor save]); -} - -/// Test that a new instance is injected each time -- (void)testNewInstance { - BAUserDataEditor *injectedEditor1 = [BAInjection injectClass:BAUserDataEditor.class]; - BAUserDataEditor *injectedEditor2 = [BAInjection injectClass:BAUserDataEditor.class]; - - XCTAssertNotEqual(injectedEditor1, injectedEditor2); - - BatchUserDataEditor *editor1 = [BatchUser editor]; - BatchUserDataEditor *editor2 = [BatchUser editor]; - - XCTAssertNotEqual(editor1, editor2); - - // Unit tests should test functionality and not ivars, but we really - // want to make sure that the backing editor is unique - XCTAssertNotEqual([editor1 valueForKey:@"_backingImpl"], [editor2 valueForKey:@"_backingImpl"]); -} - -@end diff --git a/Sources/batchTests/Modules/User/batchUserEmailSubscriptionTests.m b/Sources/batchTests/Modules/User/batchUserEmailSubscriptionTests.m deleted file mode 100644 index ad82ea7..0000000 --- a/Sources/batchTests/Modules/User/batchUserEmailSubscriptionTests.m +++ /dev/null @@ -1,92 +0,0 @@ -// -// BatchTests -// -// Copyright © Batch.com. All rights reserved. -// - -#import -#import "BATrackerCenter.h" -#import "OCMock.h" - -@interface batchUserEmailSubscriptionTests : XCTestCase - -@end - -@implementation batchUserEmailSubscriptionTests { - id userMock; - id trackerCenterMock; -} - -- (void)setUp { - userMock = OCMClassMock([BatchUser class]); - trackerCenterMock = OCMClassMock([BATrackerCenter class]); - OCMStub([trackerCenterMock instance]).andReturn(trackerCenterMock); - OCMStub([userMock identifier]).andReturn(@"test_id"); -} - -- (void)tearDown { - [userMock stopMocking]; - [trackerCenterMock stopMocking]; -} - -- (void)testSendEmailOnly { - BAUserEmailSubscription *emailSubscription = [[BAUserEmailSubscription alloc] init]; - [emailSubscription setEmail:@"test@batch.com"]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_EMAIL_CHANGED" parameters:self.eventEmailOnly]); - [emailSubscription sendEmailSubscriptionEvent]; - OCMVerifyAll(trackerCenterMock); -} - -- (void)testSendEmailNullOnly { - BAUserEmailSubscription *emailSubscription = [[BAUserEmailSubscription alloc] init]; - [emailSubscription setEmail:nil]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_EMAIL_CHANGED" parameters:self.eventEmailNullOnly]); - [emailSubscription sendEmailSubscriptionEvent]; - OCMVerifyAll(trackerCenterMock); -} - -- (void)testSendEmailSubscriptionOnly { - BAUserEmailSubscription *emailSubscription = [[BAUserEmailSubscription alloc] init]; - [emailSubscription setEmailSubscriptionState:BatchEmailSubscriptionStateSubscribed forKind:BAEmailKindMarketing]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_EMAIL_CHANGED" parameters:self.eventEmailSubscriptionOnly]); - [emailSubscription sendEmailSubscriptionEvent]; - OCMVerifyAll(trackerCenterMock); -} - -- (void)testSendEmailSubscriptionFull { - BAUserEmailSubscription *emailSubscription = [[BAUserEmailSubscription alloc] init]; - [emailSubscription setEmail:@"test@batch.com"]; - [emailSubscription setEmailSubscriptionState:BatchEmailSubscriptionStateUnsubscribed forKind:BAEmailKindMarketing]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_EMAIL_CHANGED" parameters:self.eventEmailSubscriptionFull]); - [emailSubscription sendEmailSubscriptionEvent]; - OCMVerifyAll(trackerCenterMock); -} - -- (NSDictionary *)eventEmailOnly { - return @{@"custom_id" : @"test_id", @"email" : @"test@batch.com"}; -} - -- (NSDictionary *)eventEmailNullOnly { - return @{@"custom_id" : @"test_id", @"email" : [NSNull null]}; -} - -- (NSDictionary *)eventEmailSubscriptionOnly { - return @{ - @"custom_id" : @"test_id", - @"subscriptions" : @{ - @"marketing" : @"subscribed", - } - }; -} - -- (NSDictionary *)eventEmailSubscriptionFull { - return @{ - @"custom_id" : @"test_id", - @"email" : @"test@batch.com", - @"subscriptions" : @{ - @"marketing" : @"unsubscribed", - } - }; -} - -@end diff --git a/Sources/batchTests/Modules/User/batchUserTests.m b/Sources/batchTests/Modules/User/batchUserTests.m index 34fa76f..dbf281d 100644 --- a/Sources/batchTests/Modules/User/batchUserTests.m +++ b/Sources/batchTests/Modules/User/batchUserTests.m @@ -7,8 +7,8 @@ #import #import "BAInjection.h" +#import "BAInstallDataEditor.h" #import "BAParameter.h" -#import "BAUserDataEditor.h" #import "BAUserDatasourceProtocol.h" #import "BAUserSQLiteDatasource.h" #import "Batch.h" @@ -17,7 +17,7 @@ @interface batchUserTests : XCTestCase -@property (nonatomic) BAUserDataEditor *editor; +@property (nonatomic) BAInstallDataEditor *editor; @property BAOverlayedInjectable *datasourceOverlay; @end @@ -46,10 +46,10 @@ - (void)setUp { return dataSourceToUse; }]; - _editor = [BAUserDataEditor new]; + _editor = [BAInstallDataEditor new]; // Mock editor to allow saving - BAUserDataEditor *partialMock = OCMPartialMock(_editor); + BAInstallDataEditor *partialMock = OCMPartialMock(_editor); OCMStub([partialMock canSave])._andReturn([NSNumber numberWithBool:YES]); // Clear all saved parameters before each test @@ -64,9 +64,9 @@ - (void)tearDown { - (void)testAttributesRead { // Set attributes - [_editor setAttribute:[NSDate new] forKey:@"today"]; - [_editor setAttribute:@3.2 forKey:@"float_value"]; - [_editor setAttribute:@5 forKey:@"int_value"]; + [_editor setDateAttribute:[NSDate new] forKey:@"today" error:nil]; + [_editor setDoubleAttribute:3.2 forKey:@"float_value" error:nil]; + [_editor setIntegerAttribute:5 forKey:@"int_value" error:nil]; XCTestExpectation *exp = [self expectationWithDescription:@"testing attributes read"]; [_editor save:^{ @@ -260,47 +260,4 @@ - (void)testCustomDataLimits { XCTAssertNil([BatchUser identifier]); } -- (void)testAttributionIDChangedEvent { - // Ensure event is not sent when attribution id is not a valid UUID - XCTestExpectation *exp1 = [self expectationWithDescription:@"Wait for editor stacking operations"]; - [_editor setAttributionIdentifier:@"WRONG-UUID"]; - OCMReject([trackerCenterMock trackPrivateEvent:@"_ATTRIBUTION_ID_CHANGED" - parameters:@{@"attribution_id" : @"WRONG-UUID"}]); - [_editor save:^{ - [exp1 fulfill]; - }]; - [self waitForExpectations:@[ exp1 ] timeout:3.0]; - - // Ensure event is sent when attribution id is a valid UUID - XCTestExpectation *exp2 = [self expectationWithDescription:@"Wait for editor stacking operations"]; - [_editor setAttributionIdentifier:@"EA7583CD-A667-48BC-B806-42ECB2B48606"]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_ATTRIBUTION_ID_CHANGED" - parameters:@{@"attribution_id" : @"EA7583CD-A667-48BC-B806-42ECB2B48606"}]); - [_editor save:^{ - [exp2 fulfill]; - }]; - [self waitForExpectations:@[ exp2 ] timeout:3.0]; - - // Ensure event is not sent when attribution id did not changed - [_editor setAttributionIdentifier:@"EA7583CD-A667-48BC-B806-42ECB2B48606"]; - OCMReject([trackerCenterMock trackPrivateEvent:@"_ATTRIBUTION_ID_CHANGED" - parameters:@{@"attribution_id" : @"EA7583CD-A667-48BC-B806-42ECB2B48606"}]); - XCTestExpectation *exp3 = [self expectationWithDescription:@"Wait for editor stacking operations"]; - [_editor save:^{ - [exp3 fulfill]; - }]; - [self waitForExpectations:@[ exp3 ] timeout:3.0]; - - // Ensure event is sent when attribution id is nil (reset) - XCTestExpectation *exp4 = [self expectationWithDescription:@"Wait for editor stacking operations"]; - [_editor setAttributionIdentifier:nil]; - OCMExpect([trackerCenterMock trackPrivateEvent:@"_ATTRIBUTION_ID_CHANGED" - parameters:@{@"attribution_id" : [NSNull null]}]); - [_editor save:^{ - [exp4 fulfill]; - }]; - [self waitForExpectations:@[ exp4 ] timeout:3.0]; - OCMVerifyAll(trackerCenterMock); -} - @end diff --git a/Sources/batchTests/Modules/User/userDataManagerTests.swift b/Sources/batchTests/Modules/User/userDataManagerTests.swift new file mode 100644 index 0000000..d4f0bb2 --- /dev/null +++ b/Sources/batchTests/Modules/User/userDataManagerTests.swift @@ -0,0 +1,24 @@ +@testable import Batch +import Batch.Batch_Private +import Foundation +import XCTest + +class UserDataManagerTests: XCTestCase { + func testModernAttributeMethods() async throws { + let datasource = MockUserDatasource() + + let overlay = BAInjection.overlayProtocol(BAUserDatasourceProtocol.self, returnedInstance: datasource) + defer { removeOverlay(overlay) } + + datasource.expect().call( + datasource.clearTags() + ) + datasource.expect().call( + datasource.clearAttributes() + ) + + await BAUserDataManager._performClearRemoteInstallationData() + + datasource.verify() + } +} diff --git a/Sources/batchTests/Modules/batchUserDataEditorTests.swift b/Sources/batchTests/Modules/batchUserDataEditorTests.swift index a64811a..e8992a6 100644 --- a/Sources/batchTests/Modules/batchUserDataEditorTests.swift +++ b/Sources/batchTests/Modules/batchUserDataEditorTests.swift @@ -42,7 +42,7 @@ class UserDataEditorTests: XCTestCase { datasource.setURLAttribute(Arg.eq(url!), forKey: Arg.eq("urlattr")) ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() try editor.setAttribute(true, forKey: "boolattr") try editor.setAttribute(20 as Int, forKey: "intattr") try editor.setAttribute(20 as Int64, forKey: "longlongattr") @@ -77,7 +77,7 @@ class UserDataEditorTests: XCTestCase { datasource.setDoubleAttribute(Arg.eq(Double(1.234 as Float)), forKey: Arg.eq("floatattr")) ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() try editor.setAttribute(NSNumber(value: 21), forKey: "numberattr") try editor.setAttribute(NSNumber(value: Int64.max), forKey: "numberlongattr") try editor.setAttribute(NSNumber(value: true), forKey: "boolattr") @@ -109,7 +109,7 @@ class UserDataEditorTests: XCTestCase { count: 1 ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() editor.removeTag("f0o", fromCollection: "bar") editor.removeAttribute(forKey: "f0obar") @@ -163,7 +163,7 @@ class UserDataEditorTests: XCTestCase { count: 0 ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() for key in invalidKeys { assertThrowsError(code: .invalidKey, try editor.setAttribute(true, forKey: key)) assertThrowsError(code: .invalidKey, try editor.setAttribute(20 as Int, forKey: key)) @@ -202,10 +202,10 @@ class UserDataEditorTests: XCTestCase { count: 0 ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() for key in invalidKeys { editor.addTag("foo", inCollection: key) - editor.setAttribute("2" as NSString, forKey: key) + try? editor.setAttribute("2", forKey: key) } editor.addTag("lorem ipsum dolor this is a way too long string blabla qsdqdsqdsdqsdqsdqsd", inCollection: "bar") @@ -231,7 +231,7 @@ class UserDataEditorTests: XCTestCase { datasource.removeAttributeNamed(Arg.eq("foo")) ) - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() editor.clearAttributes() editor.clearTags() editor.clearTagCollection("foo") @@ -244,7 +244,7 @@ class UserDataEditorTests: XCTestCase { func testUserOperationQueue() { do { - let editor = BAUserDataEditor() + let editor = BAInstallDataEditor() try editor.setAttribute("test", forKey: "test") editor.save() try editor.setAttribute("test2", forKey: "test2") @@ -253,125 +253,16 @@ class UserDataEditorTests: XCTestCase { } catch {} } - func testSetEmail() throws { - let editor = BAUserDataEditor() - assertThrowsError(code: .internal, try editor.setEmail("test@batch.com")) - editor.setIdentifier(nil) - assertThrowsError(code: .internal, try editor.setEmail("test@batch.com")) - editor.setIdentifier("testid") - assertThrowsError(code: .invalidValue, try editor.setEmail("test@batchcom")) - } - func assertThrowsError( - code: BatchUserDataEditorError.Code, _ expression: @escaping @autoclosure () throws -> Void, + code: BAInstallDataEditorError.Code, _ expression: @escaping @autoclosure () throws -> Void, file: StaticString = #filePath, line: UInt = #line ) { XCTAssertThrowsError(try expression(), file: file, line: line) { err in - if let err = err as? BatchUserDataEditorError { + if let err = err as? BAInstallDataEditorError { XCTAssertEqual(err.code, code, file: file, line: line) } else { - XCTFail("Error should be a BatchUserDataEditorError", file: file, line: line) + XCTFail("Error should be a BAInstallDataEditorError", file: file, line: line) } } } } - -class MockUserDatasource: Mock, BAUserDatasourceProtocol { - func close() { - super.call() - } - - func clear() { - super.call() - } - - func acquireTransactionLock(withChangeset changeset: Int64) -> Bool { - super.call(changeset) - return true - } - - func commitTransaction() -> Bool { - super.call() - return true - } - - func rollbackTransaction() -> Bool { - super.call() - return true - } - - func setLongLongAttribute(_ attribute: Int64, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func setDoubleAttribute(_ attribute: Double, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func setBoolAttribute(_ attribute: Bool, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func setStringAttribute(_ attribute: String, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func setDateAttribute(_ attribute: Date, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func setURLAttribute(_ attribute: URL, forKey key: String) -> Bool { - super.call(attribute, key) - return true - } - - func removeAttributeNamed(_ attribute: String) -> Bool { - super.call(attribute) - return true - } - - func addTag(_ tag: String, toCollection collection: String) -> Bool { - super.call(tag, collection) - return true - } - - func removeTag(_ tag: String, fromCollection collection: String) -> Bool { - super.call(tag, collection) - return true - } - - func clearTags() -> Bool { - super.call() - return true - } - - func clearTags(fromCollection collection: String) -> Bool { - super.call(collection) - return true - } - - func clearAttributes() -> Bool { - super.call() - return true - } - - func attributes() -> [String: BAUserAttribute] { - super.call() - return [:] - } - - func tagCollections() -> [String: Set] { - super.call() - return [:] - } - - func printDebugDump() -> String { - super.call() - return "mock" - } -} diff --git a/Sources/batchTests/Modules/mockUserDatasource.swift b/Sources/batchTests/Modules/mockUserDatasource.swift new file mode 100644 index 0000000..e5c5fc6 --- /dev/null +++ b/Sources/batchTests/Modules/mockUserDatasource.swift @@ -0,0 +1,110 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import Foundation +import InstantMock + +class MockUserDatasource: Mock, BAUserDatasourceProtocol { + func close() { + super.call() + } + + func clear() { + super.call() + } + + func acquireTransactionLock(withChangeset changeset: Int64) -> Bool { + super.call(changeset) + return true + } + + func commitTransaction() -> Bool { + super.call() + return true + } + + func rollbackTransaction() -> Bool { + super.call() + return true + } + + func setLongLongAttribute(_ attribute: Int64, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func setDoubleAttribute(_ attribute: Double, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func setBoolAttribute(_ attribute: Bool, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func setStringAttribute(_ attribute: String, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func setDateAttribute(_ attribute: Date, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func setURLAttribute(_ attribute: URL, forKey key: String) -> Bool { + super.call(attribute, key) + return true + } + + func removeAttributeNamed(_ attribute: String) -> Bool { + super.call(attribute) + return true + } + + func addTag(_ tag: String, toCollection collection: String) -> Bool { + super.call(tag, collection) + return true + } + + func removeTag(_ tag: String, fromCollection collection: String) -> Bool { + super.call(tag, collection) + return true + } + + func clearTags() -> Bool { + super.call() + return true + } + + func clearTags(fromCollection collection: String) -> Bool { + super.call(collection) + return true + } + + func clearAttributes() -> Bool { + super.call() + return true + } + + func attributes() -> [String: BAUserAttribute] { + super.call() + return [:] + } + + func tagCollections() -> [String: Set] { + super.call() + return [:] + } + + func printDebugDump() -> String { + super.call() + return "mock" + } +} diff --git a/Sources/batchTests/XCTest+Injection.swift b/Sources/batchTests/XCTest+Injection.swift new file mode 100644 index 0000000..14e276d --- /dev/null +++ b/Sources/batchTests/XCTest+Injection.swift @@ -0,0 +1,31 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch.Batch_Private +import Foundation +import XCTest + +/// XCTestCase extension to handle BAInjection lifetimes +extension XCTestCase { + /// Extends an BAOverlayedInjectable lifetime so that it is not deallocated instantly. + /// Meant to be used in a defer {} for async tests + /// Use: + /// ```swift + /// class MyTest: XCTestCase { + /// func test() async { + /// let injectable = BAInjection.overlayxxxx + /// defer { removeOverlay(injectable) } + /// await runMyTest + /// } + /// } + /// ``` + func removeOverlay(_ injectable: BAOverlayedInjectable) { + // This method doesn't need to do anything. The mere fact that the variable + // is retained to call this method in a defer prevents it from being + // deallocated + withExtendedLifetime(injectable) {} + } +} diff --git a/Sources/batchTests/XCTest+Threading.swift b/Sources/batchTests/XCTest+Threading.swift new file mode 100644 index 0000000..29559b1 --- /dev/null +++ b/Sources/batchTests/XCTest+Threading.swift @@ -0,0 +1,42 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch.Batch_Private +import Foundation +import XCTest + +/// XCTestCase extension to help with threading +public extension XCTestCase { + /// Waits for the main thread to consume its tasks before continuing. Useful for tests that need to wait on something + /// that has been dispatched on the main thread. + @objc + func waitForMainThreadLoop() { + // Since some tests use methods that schedules async work on the main thread, we have + // to perform a little dance to correctly test the behaviour + // To work around this, we schedule something to run on the main thread + // AFTER other work has been submitted, and wait for our dummy + // task to finish + let expectation = self.expectation(description: "Wait for a main thread loop run") + DispatchQueue.main.async { + expectation.fulfill() + } + wait(for: [expectation], timeout: 3.0) + } + + @objc + func waitForQueueLoop(queue: DispatchQueue) { + // Since some tests use methods that schedules async work on dispatch queue, we have + // to perform a little dance to correctly test the behaviour + // To work around this, we schedule something to run on the given queue + // AFTER other work has been submitted, and wait for our dummy + // task to finish + let expectation = self.expectation(description: "Wait for a dispatch queue loop run") + queue.async { + expectation.fulfill() + } + wait(for: [expectation], timeout: 3.0) + } +} diff --git a/Sources/batchTests/batchCoreTests.m b/Sources/batchTests/batchCoreTests.m index 5c2e3fe..49f5c26 100644 --- a/Sources/batchTests/batchCoreTests.m +++ b/Sources/batchTests/batchCoreTests.m @@ -39,38 +39,29 @@ - (void)testPublicMethods { id unNotificationMock = OCMClassMock([UNUserNotificationCenter class]); OCMExpect([unNotificationMock currentNotificationCenter]).andReturn(nil); - BOOL dev; /*** Start ***/ - // Check development mode. - dev = [Batch isRunningInDevelopmentMode]; - XCTAssertFalse(dev, @"Dev mode should be false."); - // Start Batch and stop it with different keys. // We can't test the feedback on key error, but we can test that it does not crash. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wall" - [Batch startWithAPIKey:nil]; + [BatchSDK startWithAPIKey:nil]; #pragma clang diagnostic pop [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]; [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification object:nil]; - [Batch startWithAPIKey:@""]; + [BatchSDK startWithAPIKey:@""]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]; [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification object:nil]; - [Batch startWithAPIKey:@"MYDEVKEY"]; + [BatchSDK startWithAPIKey:@"MYDEVKEY"]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]; [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification object:nil]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]; - - // Check development mode. - dev = [Batch isRunningInDevelopmentMode]; - XCTAssertFalse(dev, @"Dev mode should be false."); } - (void)testLoggerDelegate { @@ -78,13 +69,13 @@ - (void)testLoggerDelegate { XCTAssertNil(delegate.message); - [Batch setLoggerDelegate:delegate]; + [BatchSDK setLoggerDelegate:delegate]; // Force logging ((void (*)(id, SEL))objc_msgSend)(NSClassFromString(@"BALogger"), NSSelectorFromString(@"enableInternalLogs")); // Trigger a message - [Batch optIn]; + [BatchSDK optIn]; XCTAssertNotNil(delegate.message); @@ -94,7 +85,7 @@ - (void)testLoggerDelegate { ((void (*)(id, SEL))objc_msgSend)(NSClassFromString(@"BALogger"), NSSelectorFromString(@"disableInternalLogs")); // Trigger a message - [Batch optIn]; + [BatchSDK optIn]; XCTAssertNil(delegate.message); } diff --git a/Sources/batchTests/batchEventAttributesTests.swift b/Sources/batchTests/batchEventAttributesTests.swift new file mode 100644 index 0000000..0c38f15 --- /dev/null +++ b/Sources/batchTests/batchEventAttributesTests.swift @@ -0,0 +1,60 @@ +// +// batchEventAttributesTests.swift +// Batch +// +// Copyright © Batch.com. All rights reserved. +// + +import Batch +import Foundation +import XCTest + +class batchEventAttributesTests: XCTestCase { + func testValidData() { + let attributes = BatchEventAttributes() + + let now = Date(timeIntervalSince1970: 1_589_466_748.930) + let url = URL(string: "https://batch.com") + attributes.put(["foo", "BAR", "baz"], forKey: "$tags") + attributes.put(1, forKey: "int") + attributes.put(1.0 as Float, forKey: "float") + attributes.put(1.0 as Double, forKey: "double") + attributes.put(true, forKey: "bool") + attributes.put("foobar", forKey: "string") + attributes.put(" 456 ", forKey: "123") + attributes.put(now, forKey: "now") + attributes.put(url!, forKey: "url") + + let internalRepresentation = try! BATEventAttributesSerializer.serialize(eventAttributes: attributes) + let tags = internalRepresentation["tags"] as! [String] + let jsonAttributes = internalRepresentation["attributes"] as! [String: AnyObject] + + XCTAssertTrue(tags.contains("foo")) + XCTAssertTrue(tags.contains("bar")) + XCTAssertTrue(tags.contains("baz")) + + XCTAssertEqual(1, jsonAttributes["int.i"] as! Int) + XCTAssertEqual(1.0 as Float, jsonAttributes["float.f"] as! Float) + XCTAssertEqual(1.0 as Double, jsonAttributes["double.f"] as! Double) + XCTAssertEqual(true, jsonAttributes["bool.b"] as! Bool) + XCTAssertEqual("foobar", jsonAttributes["string.s"] as! String) + XCTAssertEqual(" 456 ", jsonAttributes["123.s"] as! String) + XCTAssertEqual(1_589_466_748_930, jsonAttributes["now.t"] as! Int64) + XCTAssertEqual("https://batch.com", jsonAttributes["url.u"] as! String) + + XCTAssertNil(internalRepresentation["converted"]) + } + + func testInvalidData() { + let attributes = BatchEventAttributes() + + attributes.put( + "A way too long string that goes for quiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiite too long" + + "Lorem ipsum dolor and other various stuff.", forKey: "string" + ) + attributes.put("foobar", forKey: "invalid_key%%%") + attributes.put("foobar", forKey: "key_that_is_too_long_really_it_should_be_more_than_thirty_chars") + attributes.put(URL(string: "batch.com")!, forKey: "url_without_scheme") + let _ = try! BATEventAttributesSerializer.serialize(eventAttributes: attributes) + } +} diff --git a/Sources/batchTests/batchEventDataTests.swift b/Sources/batchTests/batchEventDataTests.swift deleted file mode 100644 index e904476..0000000 --- a/Sources/batchTests/batchEventDataTests.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// batchEventDataTests.swift -// Batch -// -// Copyright © Batch.com. All rights reserved. -// - -import Batch -import Foundation -import XCTest - -class BatchEventDataTests: XCTestCase { - func testValidData() { - let data = BatchEventData() - data.add(tag: "foo") - data.add(tag: "BAR") - data.add(tag: "baz") - - let now = Date(timeIntervalSince1970: 1_589_466_748.930) - let url = URL(string: "https://batch.com") - data.put(1, forKey: "int") - data.put(1.0 as Float, forKey: "float") - data.put(1.0 as Double, forKey: "double") - data.put(true, forKey: "bool") - data.put("foobar", forKey: "string") - data.put(" 456 ", forKey: "123") - data.put(now, forKey: "now") - data.put(url!, forKey: "url") - - let internalRepresentation = data._internalDictionaryRepresentation() - let tags = internalRepresentation["tags"] as! [String] - let attributes = internalRepresentation["attributes"] as! [String: AnyObject] - - XCTAssertTrue(tags.contains("foo")) - XCTAssertTrue(tags.contains("bar")) - XCTAssertTrue(tags.contains("baz")) - - XCTAssertEqual(1, attributes["int.i"] as! Int) - XCTAssertEqual(1.0 as Float, attributes["float.f"] as! Float) - XCTAssertEqual(1.0 as Double, attributes["double.f"] as! Double) - XCTAssertEqual(true, attributes["bool.b"] as! Bool) - XCTAssertEqual("foobar", attributes["string.s"] as! String) - XCTAssertEqual(" 456 ", attributes["123.s"] as! String) - XCTAssertEqual(1_589_466_748_930, attributes["now.t"] as! Int64) - XCTAssertEqual("https://batch.com", attributes["url.u"] as! String) - - XCTAssertNil(internalRepresentation["converted"]) - } - - func testSizeLimits() { - let data = BatchEventData() - - for i in 0 ... 20 { - data.add(tag: String(i)) - data.put(i, forKey: String(i)) - } - - let internalRepresentation = data._internalDictionaryRepresentation() - let tags = internalRepresentation["tags"] as! [String] - let attributes = internalRepresentation["attributes"] as! [String: AnyObject] - - XCTAssertEqual(10, tags.count) - XCTAssertEqual(15, attributes.count) - } - - func testOverwrite() { - // Tests that users can still overwite keys even if they hit the limit - // Issue #57 - let data = BatchEventData() - - for i in 0 ... 14 { - data.put(i, forKey: String(i)) - } - data.put("foobar", forKey: "2") - - let internalRepresentation = data._internalDictionaryRepresentation() - let attributes = internalRepresentation["attributes"] as! [String: AnyObject] - - XCTAssertEqual(15, attributes.count) - XCTAssertEqual("foobar", attributes["2.s"] as! String) - } - - func testInvalidData() { - let data = BatchEventData() - - data.add( - tag: "A way too long string that goes for quiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiite too long" - + "Lorem ipsum dolor and other various stuff.") - data.add(tag: "") - - data.put( - "A way too long string that goes for quiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiite too long" - + "Lorem ipsum dolor and other various stuff.", forKey: "string" - ) - data.put("foobar", forKey: "invalid_key%%%") - data.put("foobar", forKey: "key_that_is_too_long_really_it_should_be_more_than_thirty_chars") - data.put(URL(string: "batch.com")!, forKey: "url_without_scheme") - let internalRepresentation = data._internalDictionaryRepresentation() - let tags = internalRepresentation["tags"] as! [String] - let attributes = internalRepresentation["attributes"] as! [String: AnyObject] - - XCTAssertEqual(0, tags.count) - XCTAssertEqual(0, attributes.count) - } - - func testLegacyDataConversion() { - let legacyData: [String: Any] = [ - "int": Int(1), - "long": Int(1), - "float": Float(1.0), - "double": Double(1.0), - "bool": true, - "string": "foobar", - ] - - let data = BatchEventData() - data._copyLegacyData(legacyData) - - let internalRepresentation = data._internalDictionaryRepresentation() - let tags = internalRepresentation["tags"] as! [String] - let attributes = internalRepresentation["attributes"] as! [String: AnyObject] - - XCTAssertEqual(0, tags.count) - XCTAssertEqual(6, attributes.count) - - XCTAssertEqual(1, attributes["int.i"] as! Int) - XCTAssertEqual(1, attributes["long.i"] as! Int) - XCTAssertEqual(1.0 as Float, attributes["float.f"] as! Float) - XCTAssertEqual(1.0 as Double, attributes["double.f"] as! Double) - XCTAssertEqual(true, attributes["bool.b"] as! Bool) - XCTAssertEqual("foobar", attributes["string.s"] as! String) - - XCTAssertTrue(internalRepresentation["converted"] as! Bool) - } - - func testLegacyDataConversionOrdering() { - // This test checks that the first 10 legacy array keys are picked in a predictable way - // They should be ordered - - let value = "test" - let unorderedKeys = [ - "drLAjNhvYs", - "wNMFqBvSHe", - "xZivnkZdZv", - "ZEZVbaXwDD", - "tvwZZnHsoJ", - "nCDiIffIqq", - "bXybuzBSvX", - "uImQWnrAyw", - "dIHDhyyDsk", - "AEBVYnPTuo", - "jfzUsSnTDf", - "vhochDgxOB", - "bJZgGgwKIM", - "GvdPlhWfyT", - "HQiXZQNHLs", - "wUGgNuvdTY", - "JHLZaOOoBQ", - "vemRXpXcUK", - "MEiAzZWjga", - "FViUCTCzfE", - ] - - let legacyData: [String: String] = unorderedKeys.reduce(into: [:]) { data, key in - data[key] = value - } - - let data = BatchEventData() - data._copyLegacyData(legacyData) - - let internalRepresentation = data._internalDictionaryRepresentation() - let attributeKeys = (internalRepresentation["attributes"] as! [String: AnyObject]).keys - - // Dicts are not ordered, so we need to sort the keys beforehand, and check if they're all there - - XCTAssertEqual(15, attributeKeys.count) - - let expectedConvertedKeys = unorderedKeys.map { "\($0.lowercased()).s" }.sorted().prefix(10) - expectedConvertedKeys.forEach { - XCTAssertTrue(attributeKeys.contains($0)) - } - } -} diff --git a/Sources/batchTests/deeplinkDelegateTests.m b/Sources/batchTests/deeplinkDelegateTests.m index da35701..ad67085 100644 --- a/Sources/batchTests/deeplinkDelegateTests.m +++ b/Sources/batchTests/deeplinkDelegateTests.m @@ -8,6 +8,7 @@ #import #import "BACoreCenter.h" #import "BatchCore.h" +#import "BatchTests-Swift.h" #import "DeeplinkDelegateStub.h" #import "OCMock.h" @@ -23,7 +24,7 @@ - (void)setUp { } - (void)tearDown { - [Batch setDeeplinkDelegate:nil]; + [BatchSDK setDeeplinkDelegate:nil]; } - (void)testNoDelegate { @@ -31,6 +32,7 @@ - (void)testNoDelegate { OCMStub([uiApplicationMock sharedApplication]).andReturn(uiApplicationMock); [[BACoreCenter instance] openDeeplink:@"https://apple.fr" inApp:NO]; + [self waitForMainThreadLoop]; [self verifyOpenURL:uiApplicationMock]; } @@ -39,14 +41,14 @@ - (void)testInvalidURL { id uiApplicationMock = OCMClassMock([UIApplication class]); OCMStub([uiApplicationMock sharedApplication]).andReturn(uiApplicationMock); - [[BACoreCenter instance] openDeeplink:@"poula%" inApp:NO]; + [[BACoreCenter instance] openDeeplink:@" https://poula" inApp:NO]; [self rejectOpenURL:uiApplicationMock]; DeeplinkDelegateStub *delegate = [DeeplinkDelegateStub new]; - Batch.deeplinkDelegate = delegate; + BatchSDK.deeplinkDelegate = delegate; - [[BACoreCenter instance] openDeeplink:@"poula%" inApp:NO]; + [[BACoreCenter instance] openDeeplink:@" https://poula" inApp:NO]; [self waitForMainThreadLoop]; XCTAssertTrue(delegate.hasOpenBeenCalled); @@ -54,7 +56,7 @@ - (void)testInvalidURL { - (void)testDelegateRemoval { DeeplinkDelegateStub *delegate = [DeeplinkDelegateStub new]; - Batch.deeplinkDelegate = delegate; + BatchSDK.deeplinkDelegate = delegate; id uiApplicationMock = OCMClassMock([UIApplication class]); OCMStub([uiApplicationMock sharedApplication]).andReturn(uiApplicationMock); @@ -62,7 +64,7 @@ - (void)testDelegateRemoval { [self waitForMainThreadLoop]; XCTAssertTrue(delegate.hasOpenBeenCalled); - Batch.deeplinkDelegate = nil; + BatchSDK.deeplinkDelegate = nil; [[BACoreCenter instance] openDeeplink:@"https://apple.fr" inApp:NO]; [self waitForMainThreadLoop]; [self verifyOpenURL:uiApplicationMock]; @@ -70,7 +72,7 @@ - (void)testDelegateRemoval { - (void)testDelegate { DeeplinkDelegateStub *delegate = [DeeplinkDelegateStub new]; - Batch.deeplinkDelegate = delegate; + BatchSDK.deeplinkDelegate = delegate; id uiApplicationMock = OCMClassMock([UIApplication class]); OCMStub([uiApplicationMock sharedApplication]).andReturn(uiApplicationMock); @@ -86,11 +88,11 @@ - (void)testDelegateWeakness { // Otherwise, we can't test this accurately @autoreleasepool { DeeplinkDelegateStub *delegate = [DeeplinkDelegateStub new]; - [Batch setDeeplinkDelegate:delegate]; - XCTAssertEqual(Batch.deeplinkDelegate, delegate); + [BatchSDK setDeeplinkDelegate:delegate]; + XCTAssertEqual(BatchSDK.deeplinkDelegate, delegate); delegate = nil; } - XCTAssertNil(Batch.deeplinkDelegate); + XCTAssertNil(BatchSDK.deeplinkDelegate); } - (void)verifyOpenURL:(id)mock { @@ -101,17 +103,4 @@ - (void)rejectOpenURL:(id)mock { OCMReject([mock openURL:[OCMArg any] options:[OCMArg any] completionHandler:[OCMArg any]]); } -- (void)waitForMainThreadLoop { - // Since openDeeplink schedules async work on the main thread, we have - // to perform a little dance to correctly test the behaviour - // To work around this, we schedule something to run on the main thread - // AFTER other work has been submitted, and wait for our dummy - // task to finish - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for a main thread loop run"]; - dispatch_async(dispatch_get_main_queue(), ^{ - [expectation fulfill]; - }); - [self waitForExpectations:@[ expectation ] timeout:3.0]; -} - @end diff --git a/Sources/bridgy.config.json b/Sources/bridgy.config.json index 3436769..fce11f6 100644 --- a/Sources/bridgy.config.json +++ b/Sources/bridgy.config.json @@ -5,7 +5,7 @@ "PrivateUmbrellaHeader.h": { "path": "Batch", "recursive": true, - "ignoredNames": "^(?!.*Private)Batch.*\\.h$", + "ignoredNames": "(^(?!.*Private)Batch.*\\.h$)|(^Versions.h$)", "frameworkName": "Batch" } } diff --git a/Tools/Dockerfile.format b/Tools/Dockerfile.format index c8e67f3..a686efc 100644 --- a/Tools/Dockerfile.format +++ b/Tools/Dockerfile.format @@ -1,18 +1,22 @@ +FROM ghcr.io/nicklockwood/swiftformat:0.53.1 as swiftformat + FROM swift:jammy -ENV DEBIAN_FRONTEND noninteractive +ENV DEBIAN_FRONTEND=noninteractive + +ENV USE_SYSTEM_SWIFTFORMAT=1 # Clang-format RUN apt-get update -y && apt-get install -y clang-format-14 && \ mv /usr/bin/clang-format-14 /usr/bin/clang-format +COPY --from=swiftformat /usr/bin/swiftformat /usr/bin + COPY .clang-format /data/.clang-format COPY .clang-format-version /data/.clang-format-version COPY .swift-version /data/.swift-version COPY .swiftformat /data/.swiftformat COPY Tools /data/Tools -RUN cd /data/Tools && swift run -c release swiftformat || true - WORKDIR "/data/Tools/Scripts" ENTRYPOINT ["/bin/bash"] \ No newline at end of file diff --git a/Tools/Package.resolved b/Tools/Package.resolved index 8d6a962..be60b35 100644 --- a/Tools/Package.resolved +++ b/Tools/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "fee7e1a1ac9518cfe3c4673382368641ccbdc62c", - "version" : "0.51.7" + "revision" : "402367fbe91d6a453bc608bfc0d93b14a301a519", + "version" : "0.53.1" } } ], diff --git a/Tools/Package.swift b/Tools/Package.swift index 0317499..9905c30 100644 --- a/Tools/Package.swift +++ b/Tools/Package.swift @@ -7,7 +7,9 @@ let package = Package( name: "Tools", dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.51.7"), + + // If you bump this version, please bump it in Dockerfile.format + .package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.53.1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Tools/Scripts/format-check.sh b/Tools/Scripts/format-check.sh index b3810e1..018023e 100644 --- a/Tools/Scripts/format-check.sh +++ b/Tools/Scripts/format-check.sh @@ -25,4 +25,8 @@ SDK_FOLDER="../../sdk" find "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" -type f -name "*.[mh]" -not \( -path "${SDK_FOLDER}/Batch/Versions.h" \) -print0 | xargs -0 ${CLANGFORMAT} --dry-run --Werror -i # Format Swift code -swift run -c release swiftformat --lint "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +if [ "${USE_SYSTEM_SWIFTFORMAT}" == "1" ]; then + swiftformat --lint "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +else + swift run -c release swiftformat --lint "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +fi diff --git a/Tools/Scripts/format.sh b/Tools/Scripts/format.sh index 19d7462..0a86fbe 100755 --- a/Tools/Scripts/format.sh +++ b/Tools/Scripts/format.sh @@ -23,4 +23,8 @@ SDK_FOLDER="../../sdk" find "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" -type f -name "*.[mh]" -not \( -path "${SDK_FOLDER}/Batch/Versions.h" \) -print0 | xargs -0 ${CLANGFORMAT} -i # Format Swift code -swift run -c release swiftformat "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +if [ "${USE_SYSTEM_SWIFTFORMAT}" == "1" ]; then + swiftformat "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +else + swift run -c release swiftformat "${SDK_FOLDER}/Batch" "${SDK_FOLDER}/batchTests" +fi diff --git a/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016.png b/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016.png new file mode 100644 index 0000000000000000000000000000000000000000..dda072187c1ceaeb184c3e137c0b7256a2351f23 GIT binary patch literal 2334 zcmV+(3E}pMP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NKh)G02RA>e5SzBxsRTSNG@1+m4 zsQ88gQVOw_wti3zF~o>|7@`SSKYS&Md};KNCm%&4raT+`Il)U|2T zrVHRo>YvSHOB?_hbJX3)6NNHv$`h0SzrRZFtb-KUAnZpy}kXCGf5&Yfg(3chDO${&DO(q z#%ltcgfW(q$1H@U;zB%IQa7^12%w~`U3Hb(5$hz+OJ_hOX5xyvy1Jf@ zj*e=?r{<&Bi480ns}mqxw>ImuGmyz-+$?Y=bT-P%%NI2?H0!b{t7~0jIo{75@Z`2nJh^kP$YY<2 zjONGPX0{11UdgM(vYK04Tib&qE(c5^zr+BNv&9m(I%8NT`L$n(v>y&yaryKd@#XO^ zL`KH)E{1WM2z9$3m(x%NyzlTc|Bfj+CIrsicJWQjtks8W2&MmeKwB+pS*$rw#&*U!GR|L6?j z1^d?d+^Ck9%g4tnWW$Hw%dNA&bL2^!ZS`WENicrev}x3@1==)Rd0YiVXr@d#0k&20 zDi!Z!uup?3N4}6K3E}m@)hnyYK0;8LriHCTYN|UE6=Vzg3q!k+*XOl~)Fyf~grqB|Xkj1bw!K1Pb8+H|5!=Av2%|Xm%JlSx- zcS&q)^5p87zHnbKrggt3AN(S+daf_`@o+4Z&3!nXzVKw-V$2F`whxVolZgrFsOS=6 zy?@x3cL%xV(Rm%o2W#NAYOHPFlNY}Bm;ga_#1v}Ibdg(Td-C3(FIn`hOX#hBk&OpEx#~P$Zl5Fa4rYl<=+@aHy*QoT zfS*(!tPL<<`BvmL*sjCxvicc{J~f$`0O4#BI!di|96Jw7$&H~@SBkuJP(WVfg+>qS z8mG+==wg&V0{QiegHe7Rg!#C@k8IgoAiuWWmyDZ*U{-8@>_4s~CZJ#{6^wLRx&G1d zy8}TIS3B}Jg>ba)#*w;up2)dno@~UPcm$`@dK{T7Z-O|>br*ZG_6p2mP@QlO_cy_5 zDv1d&R>4%NjicL&6R7z-PyQJ3-KOSC8=nF9V#7Xw(}v3rk1DsZAN|~u7n?*n&kqv5 z3BQIp@(6KpMO-Ls$NrK^sst3+f?EB?i%H0n?RX;hfc)yq#F5u_cgzV+r;XqE^3H(B zU2_9s#LyV0;AU;At1n7sqKu_%Nh4Cu9gXt)2#6REw)<@q2FQx2_2OKUM?0xMc9EDs z7=(4b2*9E-H{j{pQtiq6c);;Nr%Akw(`Iv@=T4gzoL*X@aKbi?EhSSeA%aFtr?sp8 zB=!U!W;~s^LT2D)__aQfu(74|)CnkfDIiNCDyEcQ?JA?%#@P}RAg~3G((y^KxG}rCyWJN!%45fl zxwgH%y)rT~BD4(;56iY~+ltrTy?cxJVQgD3l|*6!Q4K9H^+Sga6*qH#f4|dd+rE9f z(+36yB%jYqUtgb0pFUk$T3Xy!hh@u_$&w{YCUjx3Cpbz}QYS!Q3vP8{+q%Z$3!CiO zv&VI9{rpOyZPc!9)b0XO+!7Pe4&tyls$3d8sW{U75zltb&CN16I4DPt;(Zz=4VLzk zHa0PVaK{8zaMkfoi?p@1$=0n~rLnQmCBSw}WW|aVZcjuJQyleCDyHhz7o{gY$ndvN z$p@J}8wgCDeCiJa#E8`O2o7gxu;8w^)APZVbSHe+b*~Z#Q zzR)*+LhGCPBqM)W6BATktRr~I_>{PAz>x0a#Fv4120S$qV2owegFUf07*qoM6N<$ Ef=nJ>&;S4c literal 0 HcmV?d00001 diff --git a/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016@2x.png b/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/TemplateIcon-1016@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6519b4409ec65806e196d30b804abea48d3a2e9b GIT binary patch literal 5693 zcmYLNcRbX8{QuZzo-!lOh*WZk5a*oLB{Ry%-XklVdCr+v$=)KXqRi}lM9Ai37AIs} z)*0D;zP~?yzdv5%{eC|m?|)ve=j;7=zF+C-Aek9>7ytlZ)_ACbC-T+07D zwaTUX|NKATCn-xE0N^OpP*Z;5PqA*!_$K=s3hoE7P-2_;JI&3@dlm0R7S{V$azSM;da7Vc81h))_w2RfK%_yjn`TE2AAYGLjTu+ zpHn}gK{bmh&@>RbwHm<7*Q^(rukNmgyD!z4s5xnY;gJU zxtmkmcnjWnneU^<^5$YRFU#$U}r|281i)z#?B=3C)QX;T9wC-H96HlN!$VLa&j z=!1&Bg6uq1oY47EnR!!L<4S(C;U{z)93Z|2@?P=vq#IHUCn(8&G`$(QJUBY z(8rF9jYT+)cD`aeCq;5u97L>1lfp5$Y{n~m2n%7L?FHp)eddyVsT@Qlv zp)ud?B~MruP2(F)u8mT!(zZo8s!x>F6wb`ndL|QH$#C+%u`WJ%2w~H1h1Sv z6{v=}o*ZnSpN(ol=ZEjC2uL#^P*)x?=2|YNT@u%O3AT7?IRYZxS@Gp;qje0`n30#4 z$5aRItsS~}=qWp~16_jNJd7@H<}9gv7Y#UJVePpc@b-t(l%P4wDhFmA&v055L7>8$ z39GG&(f~@*`)~(>!8fPnDXqhY6tP}FM<4k9dNqMRou2Lsc6q@lvnr+TJ z3ci2CNfYJg&6)Co0_>E4!m5xJ1`83!4?+!~b_$&ZFnz`-r8WA9Uvxd1cAAY>f8nh= zqTY^;VZ-%6Kthp{aVBUF(IyPXP!)D-m5nl>#GF5LXL;r_gYkB_`2@m(1s9W z#|DP0HyHO?9qu`!2PxM)MLnNLskbm!Gt2EfY*1U(sy9_W;C)&pz%IkN-iH#__Mb4+8p}n;+}hgU);+R5=kherBVDi_Qxzu z{Z+|$Oz%Fo|NG?a9kE*=znh@*qCkM+t(5$)x`p0;l}G?ftJ==6ZBywpJ@Lh!`dpKw zp1SGFY^bX+{HwZLT(;RPRT1eDL~$_O-GPwOh4+=zA)iR7CO>6$|8>zGHus+EA{!85 zOn@)vClPs%fS&M+y0F;Oo`=jzJ?mY5h-^fKn7Eq>SN?Nak7ODh=ihmlQOFcPE&OCW zPL9iMs&a|+xx)n5i~6Jo5F^)<_ilEAw~K5^tS2*;H#EwL6Vw^rtF>S?cRkkqYXZY8 z`f$*l9T|I~Tg!lf?_`Cw?24C9nv9d93RG$B_988ybIAT%S@hzLl9sie345ffQ?X0H z5Y7mkY&SnG8nEdZ_jjfa%Hvu+kSpxbNA#x8XENxq*7e_?a#SkQ@f20dDsH&ayF?2Z z9dn zNI~oI@^Xq%CM*iq z;-5frJz7ZST+34dtW^IdGhdUp>q}<-zyw;#36#3_biN@Y*SJ!f`YMGB-`&n&J{n7n z5w_3bbLEi1y5yQNT5fT~yY`RFsTt3%Og@+jk{@Gt1Rij{n`OQs9K%2ZFn^I^67pY( zzuoOmG?jlTsjkl(+nfQ+2vE~TjL@@YasXp{#3toZd*|n(?d4ljH++OfAZD@RKt1D{M%b=`ZJ^!HqUkoP zw|9E5Lb`B$C0%}lH4!grz~QceJ3x$pAp8^LCW<9QHOqs1$ZJ8#Og|))_ZbcPDjyA< zn(jK8h+rKYLsL=Ko(u7AmD#ZgrCvjkig(cu)sO3^ADC+zd~;kZW!V!4XXf1vD3Sqp zN3VmLBbyb==Y=>m6gfFU($fb;1QMbZ&f>U6uZ5}u8 ztx6al3UT)GHU5S9U^KeGw>AHL;XiaJ>=~N7L611-cp=iT?wVlT7}(**)^8EKeOUWO z=H#xw$)`$MJ3<%>wxgcMm)I66@fjD!a$OZo_8;_{V05taE7}v`daX4Ah5rtmXM}^C z#(D_+I!Mr29Y_5u9&~g3?J=&Z+efKdVY&q5YnZ3INmuP2ODOYAw`YofGS=vhCSUFu z_O2gxOPAhL%S&b_8Clz97e4l(L|^ca`dnsq9ibesG2)uWZw>#K*b^HrRDcpiA5$h& z-E(T`>8Xl&(TKTyXsjU`=ahJ=cqCELYEsKLrvB(3wd*_BfUD8$z_Yr4#!J!>s9$C4 z70YQsHRgJG7bH!BIBfx8N|`s?q2EI`%VH4Xigkl+e~jnnKN7+!7QZI;?xrGm8_2?I&qjWoPE>ddMdgnC{v}L3dm^vT7IYH&O zmp8zX;URM#uh2(Y2~7^x+T~*=tnhqXE6zu>f6PD|Iyy29uU8iol6l4L(WfgRPm~bQG{xjdPU-#EV z8jQl{GX14O!ou$GY9i(`wjP_u^8aX=PjY;+`1a|d-^^b9(ToriTw+1~9|_?}$DV*6 z#zM0Uqx%L3n&f4EP&$23tZ31M8XaunVf64P_pfwdexJ7$_|9SPoH*in{w-%iij0Ex z!iqiR(0E*S71)*WtQ|?Pp8ff<-YUPw?ad$1~Y7~TfrvD#ZrRp zjC~_UGPxgZ>a-o1!omEpnWm_L>kSD?Ep^}$)kZzXuQdiPSvo&j&VRJh(LS1NjZc#u zXrVTtJM!MjsB;T=$2nLWqK%sX`E@vI*f*l(q6IPsakmT!(5ylsOIrvg~2ek>7yl8{Kqn}q|yAa6uK$3 z$+i6HcWrSb-0sD@mz93$R9)1e_*0F!y=X?Yyk#xE5|VVbcsz!a=lHGsVm9I#(rAr? zYM;urm-;JDVNA|SC2DB=*+8W2qq~oFS_-VZfz-`j|r8@}SOYpmKNeqJSEdj7GJ z#Y($`xd$;jK>=Z3FmfRsXYgW;r39DHjCCdO6OXCpjW!JtZC|9Rzv}v>EIxnxoQl_@ z+ft!$+@{XL^zVeM`KXY&*WbQru!Rw8?H;p0_-hYC@!**0&kCQf)ur1P8%lXLT+CY> zT2@s+e4Gs&n|%$7^^j+!CV#O$?+~TN3q`XH`@u!;>R8sY>?JR542U%B8VRM{<}#@^ zTvYs(i>paqR1Vf{yGON(WBA%4&xW?FuvYq#r2n}9Z*x|9B4ILf98G&A z=Qj4<{7Gh}#;ln40^%}#BUBn&DEE3=R&+BlW}Hhge$zWr;f_*=XJH*pHfOMpR&T|| z#)gC?%*SFsV8hKAc?NBg4x^_?W_&;r$>68C5l5nF_SYWQZ1}gQzp-;8&1+$-0LM`z zg^I(9OKf>oUz=ZYduU*SaZ1g0mN(^>$D1+3?Gr}n`gRVFZs{~gEa-3ptqWpQ5i^#T z0yeUFK*aDn3^Tao;`Z*+Kg+CW@8SkTTxL2)bltecc!jRjr`MsxH>6d@2;xE_SQ!7# zNyWP4e&bfXluw8K79W({@$ZTaG4kLN+o!*+in<(rDJ`kF zsyLPgk)BwFOwhIpiaT!Zw5BJiJY}g((seWiY}d8ow83}sdf)NmN*Bo-Do#+GcUP3$ zyY`QBvKlh}fidU056K~P;ZMklCTAAjEUq}3y}4H3UUc~Sd&wMI^l40HyLlK0(SMk} zAPDz2PW8WmyG6pAiBuj^F`HJp3LMDlmPhp0ZI8We4DcwsjRfZDTi$oTDNU@x;}I!NSgYKpTY|goNF+3h~;v_@gVDy5wN)i{`_h zAQ`@<4VKmH;*YvwprkPL6oc%Elql~4u5@?k_EoHLk|%5t{e}+iuAt$uf=oK=;~}AW z54a6@y}=J)vx*N}U3rwE#n3 z<>d|Et#vxh|x9~bDRrirDXx99=}N${$j@MV15<+f$ZM&*QU0GUkNGQJ>}@e zrxZ->mibVVtEYykXQ5obtIpKkM*;wnAfITs>^GYBErA^}Zp!NK=ksJJCBB>nZ^H~V z0l_zYC!;*xp6D@{Sk6LmyOm^@ohFR-%9G8oT`2oFii>(D&)etCX5a5@Vl+wZT?p6p zLx%ac>%BEYdZ}14SSUHaU~OH2%~aP7p>m%JG~$utE1kuBkM~i*x36`>kx! z;q&ICMolD*;xc7*O0)i5;hsyiWSOD?vnR%E?}3AuHZD^GHiI9w32Q18pzTpt0A$$> zRfVu$=zEUUS#kh{m@ED7o`NJTTl84rkH{Q8u{64IR3lU~I(=Qe2${Da`ugypW zFw6e-EGm#xFi}XOi72U!CV$Zbj;I8q34&HO`?i(waiPTH%T<>aI(#oZkpnz?H_a4d z9af$CF>fH0dHgv?Nv~=qa^96fSOHfm3|uoek2fg}1@EE*!6WUeYL{pFpA9l2ir74b z#uLu2zL{cN{bpCSWm{=7&G>!1uOmgp-R8PYK?(nvsYi>5zr#T0(%C8{Y8G?s)}141 z8^r{Ho9#j*LH^5G-m%BoR@KZ?ib4B5v3v@4Yh$1CLza5Kx6SrvNPVcOd1?`$j>qJw z1(#aW!0hsO;h4Al{+Q-EO+G`ah=UiPezWq$BhsjkpfR=?=g{#HI#hUy< zeuM z4?zXjG`!#|c4|0foRXW>p6hHa*F~WX3*Vh!f#H|7&hmMUhdawn=jqk1QL8jY9 zLwHkN2+RwxDylkEsoI+U{3TB9O6ripCM-iSsCLD&%&U-}OwgaBwQv7NI=O2ft;I+o zFpw9h5PFZV+HI!RBDC!Ws55YGmcz(QQ24={c#d0!a`%rr(rQo`{wk|G4l34#0=_ci zug(q#AGN)^`%`WiHP)D)FD0))y1cvlRtbeo1vkRGYW2pIa<3a%1bkQt*|kNLkM&N! zK06qG}FTWJFLw; zh!g9=xt2(D#jSQOR*~r=){*^DDW7DOa)HjUq%K`=Sm{DuBP+CYT{_@`f`y0KhH1Rm zz^kvmzrWzH4IjfQmTLj`H^NSmk=8|Ykru(%k=B0vL9aWPeWCD*7D1zU$F0kbv@|i} zhc`EMX9v*D5rxQeho4ZQXTS4|v6kpZND^+K1SNSWj_#-IF`x@ZGVNN!h|fe_*a7L^ zz;Mu!>HfyAW2qLEmD<5?&AB^Y0&{X4s_>10Zr{)h+Z17EPiNgv3y4ZhXGbKD5g8D6 z&dhviOH#y~Lmps38lT>i8!7L8ea4g3w@}7~ikW;`ll1Oqt^stf{7F4N(chS$s==RT zn9=UEsE8#n$K*XeeJY3aT + + + + SupportsSwiftPackage + + Kind + Xcode.IDEFoundation.TextSubstitutionFileTemplateKind + Description + A class implementing a unit test. + Summary + A class implementing a unit test + SortOrder + 11 + BuildableType + Test + DefaultCompletionName + Test + Platforms + + Options + + + Description + Name of the unit test class + Identifier + productName + Name + Class: + NotPersisted + Yes + Required + Yes + Type + text + + + Default + XCTestCase + Description + Unit test class that the new class will be a subclass of + Identifier + testSubclass + Name + Subclass of: + Required + Yes + Type + class + Values + + XCTestCase + + + + AllowedTypes + + Swift + + public.swift-source + + + Default + Swift + Description + The implementation language + Identifier + languageChoice + MainTemplateFiles + + Swift + ___FILEBASENAME___.swift + + Name + Language: + Required + Yes + Type + popup + Values + + Swift + + + + + diff --git a/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/XCTestCaseSwift/___FILEBASENAME___.swift b/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/XCTestCaseSwift/___FILEBASENAME___.swift new file mode 100644 index 0000000..df363dc --- /dev/null +++ b/Tools/XcodeTemplates/Batch Unit Test Case Class.xctemplate/XCTestCaseSwift/___FILEBASENAME___.swift @@ -0,0 +1,21 @@ +// +// BatchTests +// +// Copyright © Batch.com. All rights reserved. +// + +@testable import Batch +import Batch.Batch_Private +import XCTest + +final class ___FILEBASENAMEASIDENTIFIER___: XCTestCase { + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + +}