diff --git a/Iterable-iOS-SDK.podspec b/Iterable-iOS-SDK.podspec index 7524de10d..a8fb91229 100644 --- a/Iterable-iOS-SDK.podspec +++ b/Iterable-iOS-SDK.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/Iterable/swift-sdk.git", :tag => s.version } s.source_files = "swift-sdk/**/*.{h,m,swift}" - s.resource_bundles = {'Iterable-iOS-SDK' => 'swift-sdk/Resources/**/*.{storyboard,xib,xcassets}' } + s.resource_bundles = {'Iterable-iOS-SDK' => 'swift-sdk/Resources/**/*.{storyboard,xib,xcassets,xcdatamodeld}' } s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.2' diff --git a/Package.swift b/Package.swift index 2c3daaddc..adfd65f05 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 import PackageDescription @@ -19,7 +19,10 @@ let package = Package( ], targets: [ .target(name: "IterableSDK", - path: "swift-sdk"), + path: "swift-sdk", + resources: [ + .process("Resources"), + ]), .target(name: "IterableAppExtensions", path: "notification-extension"), ] diff --git a/host-app/Info.plist b/host-app/Info.plist index 10f85c241..dffa2f8b6 100644 --- a/host-app/Info.plist +++ b/host-app/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - UITestApp + $(PRODUCT_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 2407920de..1beada15e 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ AC19520D231D9AC600CD5B61 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACA8D1A42196309C001B1332 /* Common.swift */; }; AC19520E231DAB7B00CD5B61 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC72A0BE20CF4CB8004D7997 /* Constants.swift */; }; AC195210231DAD6B00CD5B61 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CB31B4210960C4004ACDEC /* TestUtils.swift */; }; + AC1AA1C624EBB2DC00F29C6B /* IterableTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1AA1C524EBB2DC00F29C6B /* IterableTaskRunner.swift */; }; + AC1AA1C924EBB3C300F29C6B /* IterableNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1AA1C824EBB3C300F29C6B /* IterableNotifications.swift */; }; AC1BED9523F1D4C700FDD75F /* MiscInboxClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1BED9423F1D4C700FDD75F /* MiscInboxClasses.swift */; }; AC219C49225FD7EB00B98631 /* IterableInboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC219C48225FD7EB00B98631 /* IterableInboxViewController.swift */; }; AC219C4D225FE4C000B98631 /* InboxMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC219C4C225FE4C000B98631 /* InboxMessageViewModel.swift */; }; @@ -47,12 +49,15 @@ AC219C51225FEDBD00B98631 /* SampleInboxCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = AC219C4F225FEDBD00B98631 /* SampleInboxCell.xib */; }; AC219C532260006600B98631 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC219C522260006600B98631 /* Assets.xcassets */; }; AC2263F020CF49B8009800EB /* IterableSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = AC2263E220CF49B8009800EB /* IterableSDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC241C2224F5757C00F8F9CC /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C668320D3370600D46CC9 /* Mocks.swift */; }; AC28480A24AA44C600C1FC7F /* EndpointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC28480924AA44C600C1FC7F /* EndpointTests.swift */; }; AC28480C24AA44C600C1FC7F /* IterableSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC2263DF20CF49B8009800EB /* IterableSDK.framework */; }; AC29D05C24B5A7E000A9E019 /* CI.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC29D05B24B5A7E000A9E019 /* CI.swift */; }; AC2A2986231A7CFF0070A9C3 /* TestInAppPayloadGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC87172521A4E47E00FEA369 /* TestInAppPayloadGenerator.swift */; }; AC2A2988231CFAC40070A9C3 /* NetworkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2A2987231CFAC40070A9C3 /* NetworkTableViewController.swift */; }; AC2A298A231D44C00070A9C3 /* NetworkDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2A2989231D44C00070A9C3 /* NetworkDetailViewController.swift */; }; + AC2AED4224EBC60C000EE5F3 /* TaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */; }; + AC2AED4424EBC905000EE5F3 /* IterableTaskScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AED4324EBC905000EE5F3 /* IterableTaskScheduler.swift */; }; AC2B79F721E6A38900A59080 /* NotificationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2B79F621E6A38900A59080 /* NotificationHelper.swift */; }; AC2C667E20D3111900D46CC9 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C667D20D3111900D46CC9 /* DateProvider.swift */; }; AC2C668020D31B1F00D46CC9 /* IterableNotificationResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C667F20D31B1F00D46CC9 /* IterableNotificationResponseTests.swift */; }; @@ -64,8 +69,10 @@ AC32E16821DD55B900BD4F83 /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC32E16721DD55B900BD4F83 /* OrderedDictionary.swift */; }; AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC347B5B20E5A7E1003449CF /* APNSTypeChecker.swift */; }; AC347B6720E699FA003449CF /* IterableAppExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = AC347B6620E699D8003449CF /* IterableAppExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC3A336D24F65579008225BA /* RequestProcessorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */; }; AC3C10F9213F46A900A9B839 /* IterableLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3C10F8213F46A900A9B839 /* IterableLogging.swift */; }; AC3DD9C82142F3650046F886 /* ClassExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3DD9C72142F3650046F886 /* ClassExtensions.swift */; }; + AC3EFFF02510B8FB007F1330 /* TaskSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */; }; AC4095A422B18B9D006EF67C /* InboxViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4095A322B18B9D006EF67C /* InboxViewControllerViewModel.swift */; }; AC426226238C27DD00164121 /* IterableInboxCell+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC426225238C27DD00164121 /* IterableInboxCell+Layout.swift */; }; AC426CC4211B5497002EDBE8 /* ServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC426CC3211B5497002EDBE8 /* ServerResponse.swift */; }; @@ -73,7 +80,15 @@ AC4B039622A8743F0043185B /* InAppManager+Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4B039322A8743F0043185B /* InAppManager+Functions.swift */; }; AC4BA00224163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */; }; AC4BAE0A240BAF0E00D9121F /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = AC4BAE09240BAF0E00D9121F /* OHHTTPStubs */; }; + AC50865424C60172001DC132 /* IterableDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */; }; + AC50865624C603AC001DC132 /* IterablePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC50865524C603AC001DC132 /* IterablePersistence.swift */; }; + AC50865824C60426001DC132 /* IterableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC50865724C60426001DC132 /* IterableTask.swift */; }; + AC50865A24C60572001DC132 /* IterableCoreDataPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */; }; + AC5812F624F3A90F007E6D36 /* OfflineRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */; }; + AC5812F824F3AE8D007E6D36 /* RequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */; }; + AC5E888924E1B7CE00752321 /* OnlineRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */; }; AC64626B2140AACF0046E1BD /* IterableAPIResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */; }; + AC67AF982507481200C1E974 /* NetworkConnectivityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */; }; AC6FDD8820F4372E005D811E /* IterableAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC6FDD8720F4372E005D811E /* IterableAPI.swift */; }; @@ -116,10 +131,12 @@ AC8874AA22178BD80075B54B /* InAppContentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8874A922178BD80075B54B /* InAppContentParser.swift */; }; AC89661E2124FBCE0051A6CD /* IterableAutoRegistrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC89661D2124FBCE0051A6CD /* IterableAutoRegistrationTests.swift */; }; AC8A058924AB1FE1002C1103 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8A058824AB1FE1002C1103 /* Environment.swift */; }; + AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */; }; AC8E9268246284F800BEB68E /* DataFieldsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8E9267246284F800BEB68E /* DataFieldsHelper.swift */; }; AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC8F35A1239806B500302994 /* InboxViewControllerViewModelTests.swift */; }; AC90C4CD20D8632E00EECA5D /* IterableAppExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC90C4C420D8632D00EECA5D /* IterableAppExtensions.framework */; }; AC90C4E220D8639E00EECA5D /* ITBNotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC90C4E120D8639E00EECA5D /* ITBNotificationServiceExtension.swift */; }; + AC978D3E24FF953C00372B8C /* NetworkConnectivityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC978D3D24FF953C00372B8C /* NetworkConnectivityChecker.swift */; }; AC995F992166EE490099A184 /* CommonMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F942166EC880099A184 /* CommonMocks.swift */; }; AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F942166EC880099A184 /* CommonMocks.swift */; }; AC995F9D2167E9FD0099A184 /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F9C2167E9FD0099A184 /* CommonExtensions.swift */; }; @@ -141,6 +158,18 @@ ACB37AB124026C1E0093A8EA /* SampleInboxViewDelegateImplementations.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB37AAF240268A60093A8EA /* SampleInboxViewDelegateImplementations.swift */; }; ACB8273F22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB8273E22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift */; }; ACBDDE5C23C4EDEC0008CC4D /* InboxCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACBDDE5B23C4EDEC0008CC4D /* InboxCustomizationTests.swift */; }; + ACC362B624D16D91002C67BA /* IterableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362B524D16D91002C67BA /* IterableRequest.swift */; }; + ACC362B824D17005002C67BA /* IterableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362B724D17005002C67BA /* IterableRequestTests.swift */; }; + ACC362BA24D20BBB002C67BA /* IterableAPICallRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362B924D20BBB002C67BA /* IterableAPICallRequest.swift */; }; + ACC362BD24D21172002C67BA /* IterableAPICallTaskProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362BC24D21172002C67BA /* IterableAPICallTaskProcessor.swift */; }; + ACC362BF24D21192002C67BA /* IterableTaskProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362BE24D21192002C67BA /* IterableTaskProcessor.swift */; }; + ACC362C124D21272002C67BA /* IterableTaskResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362C024D21272002C67BA /* IterableTaskResult.swift */; }; + ACC362C324D21332002C67BA /* IterableTaskError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362C224D21332002C67BA /* IterableTaskError.swift */; }; + ACC362C524D2C190002C67BA /* TaskProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */; }; + ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F9C2167E9FD0099A184 /* CommonExtensions.swift */; }; + ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F942166EC880099A184 /* CommonMocks.swift */; }; + ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CB31B4210960C4004ACDEC /* TestUtils.swift */; }; + ACC362C924D2CA8C002C67BA /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACA8D1A42196309C001B1332 /* Common.swift */; }; ACC51A6B22A879070095E81F /* EmptyInAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4B039122A8743F0043185B /* EmptyInAppManager.swift */; }; ACC6A84F2323910D003CC4BE /* UITestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC6A84E2323910D003CC4BE /* UITestsHelper.swift */; }; ACC6A8502323910D003CC4BE /* UITestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC6A84E2323910D003CC4BE /* UITestsHelper.swift */; }; @@ -148,6 +177,7 @@ ACC87766215C20B50097E29B /* UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACC87765215C20B50097E29B /* UITests.swift */; }; ACC8776D215C23CC0097E29B /* IterableSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC2263DF20CF49B8009800EB /* IterableSDK.framework */; }; ACC8776E215C23CC0097E29B /* IterableSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AC2263DF20CF49B8009800EB /* IterableSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */; }; ACD6116C2107D004003E7F6B /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD6116B2107D004003E7F6B /* NetworkHelper.swift */; }; ACD6116E21080564003E7F6B /* IterableAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD6116D21080564003E7F6B /* IterableAPITests.swift */; }; ACDA975C23159C37004C412E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDA975B23159C37004C412E /* AppDelegate.swift */; }; @@ -165,11 +195,19 @@ ACED4C01213F50B30055A497 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACED4C00213F50B30055A497 /* LoggingTests.swift */; }; ACEDF41D2183C2EC000B9BFE /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACEDF41C2183C2EC000B9BFE /* Promise.swift */; }; ACEDF41F2183C436000B9BFE /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACEDF41E2183C436000B9BFE /* PromiseTests.swift */; }; + ACF32BDB24E3EA7C0072E2CC /* RequestProcessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */; }; + ACF40621250781F1005FD775 /* NetworkConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF40620250781F1005FD775 /* NetworkConnectivityManager.swift */; }; + ACF406232507BC72005FD775 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF406222507BC72005FD775 /* NetworkMonitor.swift */; }; + ACF406252507F90F005FD775 /* NetworkConnectivityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */; }; ACF560D620E443BF000AAC23 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF560D520E443BF000AAC23 /* AppDelegate.swift */; }; ACF560DB20E443BF000AAC23 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ACF560D920E443BF000AAC23 /* Main.storyboard */; }; ACF560DD20E443C0000AAC23 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ACF560DC20E443C0000AAC23 /* Assets.xcassets */; }; ACF560E020E443C0000AAC23 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ACF560DE20E443C0000AAC23 /* LaunchScreen.storyboard */; }; ACF560E820E55A6B000AAC23 /* IterableActionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF560E720E55A6B000AAC23 /* IterableActionContext.swift */; }; + ACFD5AB324C8179D008E497A /* PersistenceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */; }; + ACFD5ABD24C8200C008E497A /* IterableSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC2263DF20CF49B8009800EB /* IterableSDK.framework */; }; + ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */; }; + ACFD5AC824C8290E008E497A /* IterableTaskManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */; }; ACFF4287246569D300FDF10D /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F9C2167E9FD0099A184 /* CommonExtensions.swift */; }; ACFF428824656A2000FDF10D /* CommonMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F942166EC880099A184 /* CommonMocks.swift */; }; ACFF428F24656BDF00FDF10D /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC995F9C2167E9FD0099A184 /* CommonExtensions.swift */; }; @@ -241,6 +279,20 @@ remoteGlobalIDString = ACF560D220E443BF000AAC23; remoteInfo = "host-app"; }; + ACFD5ABE24C8200C008E497A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AC2263D620CF49B8009800EB /* Project object */; + proxyType = 1; + remoteGlobalIDString = AC2263DE20CF49B8009800EB; + remoteInfo = "swift-sdk"; + }; + ACFD5AC324C82013008E497A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AC2263D620CF49B8009800EB /* Project object */; + proxyType = 1; + remoteGlobalIDString = ACF560D220E443BF000AAC23; + remoteInfo = "host-app"; + }; ACFF428B24656BDF00FDF10D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AC2263D620CF49B8009800EB /* Project object */; @@ -326,6 +378,8 @@ AC0A45372179300D0040394F /* host-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "host-app.entitlements"; sourceTree = ""; }; AC1670CC2230A91C00989F8E /* InboxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxTests.swift; sourceTree = ""; }; AC1712882416AEF400F2BB0E /* WebViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProtocol.swift; sourceTree = ""; }; + AC1AA1C524EBB2DC00F29C6B /* IterableTaskRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskRunner.swift; sourceTree = ""; }; + AC1AA1C824EBB3C300F29C6B /* IterableNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableNotifications.swift; sourceTree = ""; }; AC1BED9423F1D4C700FDD75F /* MiscInboxClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiscInboxClasses.swift; sourceTree = ""; }; AC219C48225FD7EB00B98631 /* IterableInboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInboxViewController.swift; sourceTree = ""; }; AC219C4C225FE4C000B98631 /* InboxMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxMessageViewModel.swift; sourceTree = ""; }; @@ -341,6 +395,8 @@ AC29D05B24B5A7E000A9E019 /* CI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CI.swift; sourceTree = ""; }; AC2A2987231CFAC40070A9C3 /* NetworkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkTableViewController.swift; sourceTree = ""; }; AC2A2989231D44C00070A9C3 /* NetworkDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDetailViewController.swift; sourceTree = ""; }; + AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRunnerTests.swift; sourceTree = ""; }; + AC2AED4324EBC905000EE5F3 /* IterableTaskScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskScheduler.swift; sourceTree = ""; }; AC2B79F621E6A38900A59080 /* NotificationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHelper.swift; sourceTree = ""; }; AC2C667D20D3111900D46CC9 /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; AC2C667F20D31B1F00D46CC9 /* IterableNotificationResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableNotificationResponseTests.swift; sourceTree = ""; }; @@ -352,15 +408,25 @@ AC32E16721DD55B900BD4F83 /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; AC347B5B20E5A7E1003449CF /* APNSTypeChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSTypeChecker.swift; sourceTree = ""; }; AC347B6620E699D8003449CF /* IterableAppExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IterableAppExtensions.h; sourceTree = ""; }; + AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProcessorUtil.swift; sourceTree = ""; }; AC3C10F8213F46A900A9B839 /* IterableLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableLogging.swift; sourceTree = ""; }; AC3DD9C72142F3650046F886 /* ClassExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassExtensions.swift; sourceTree = ""; }; + AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSchedulerTests.swift; sourceTree = ""; }; AC4095A322B18B9D006EF67C /* InboxViewControllerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewControllerViewModel.swift; sourceTree = ""; }; AC426225238C27DD00164121 /* IterableInboxCell+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IterableInboxCell+Layout.swift"; sourceTree = ""; }; AC426CC3211B5497002EDBE8 /* ServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerResponse.swift; sourceTree = ""; }; AC4B039122A8743F0043185B /* EmptyInAppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyInAppManager.swift; sourceTree = ""; }; AC4B039322A8743F0043185B /* InAppManager+Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InAppManager+Functions.swift"; sourceTree = ""; }; AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableHtmlMessageViewControllerTests.swift; sourceTree = ""; }; + AC50865324C60172001DC132 /* IterableDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = IterableDataModel.xcdatamodel; sourceTree = ""; }; + AC50865524C603AC001DC132 /* IterablePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterablePersistence.swift; sourceTree = ""; }; + AC50865724C60426001DC132 /* IterableTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTask.swift; sourceTree = ""; }; + AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableCoreDataPersistence.swift; sourceTree = ""; }; + AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineRequestProcessor.swift; sourceTree = ""; }; + AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProcessor.swift; sourceTree = ""; }; + AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineRequestProcessor.swift; sourceTree = ""; }; AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPIResponseTests.swift; sourceTree = ""; }; + AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityCheckerTests.swift; sourceTree = ""; }; AC684A85222EF75C00F29749 /* InAppMessageParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessageParser.swift; sourceTree = ""; }; AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppDisplayer.swift; sourceTree = ""; }; AC6FDD8720F4372E005D811E /* IterableAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPI.swift; sourceTree = ""; }; @@ -402,6 +468,7 @@ AC8874A922178BD80075B54B /* InAppContentParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppContentParser.swift; sourceTree = ""; }; AC89661D2124FBCE0051A6CD /* IterableAutoRegistrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAutoRegistrationTests.swift; sourceTree = ""; }; AC8A058824AB1FE1002C1103 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; + AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataUtil.swift; sourceTree = ""; }; AC8E9267246284F800BEB68E /* DataFieldsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFieldsHelper.swift; sourceTree = ""; }; AC8F35A1239806B500302994 /* InboxViewControllerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewControllerViewModelTests.swift; sourceTree = ""; }; AC90C4C420D8632D00EECA5D /* IterableAppExtensions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IterableAppExtensions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -409,6 +476,7 @@ AC90C4CC20D8632E00EECA5D /* notification-extension-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "notification-extension-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; AC90C4D520D8632E00EECA5D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC90C4E120D8639E00EECA5D /* ITBNotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ITBNotificationServiceExtension.swift; sourceTree = ""; }; + AC978D3D24FF953C00372B8C /* NetworkConnectivityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityChecker.swift; sourceTree = ""; }; AC98294A20D9D65E00796DAA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; AC995F942166EC880099A184 /* CommonMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMocks.swift; sourceTree = ""; }; AC995F9C2167E9FD0099A184 /* CommonExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonExtensions.swift; sourceTree = ""; }; @@ -423,11 +491,20 @@ ACB37AAF240268A60093A8EA /* SampleInboxViewDelegateImplementations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleInboxViewDelegateImplementations.swift; sourceTree = ""; }; ACB8273E22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableHtmlMessageViewController.swift; sourceTree = ""; }; ACBDDE5B23C4EDEC0008CC4D /* InboxCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxCustomizationTests.swift; sourceTree = ""; }; + ACC362B524D16D91002C67BA /* IterableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequest.swift; sourceTree = ""; }; + ACC362B724D17005002C67BA /* IterableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequestTests.swift; sourceTree = ""; }; + ACC362B924D20BBB002C67BA /* IterableAPICallRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPICallRequest.swift; sourceTree = ""; }; + ACC362BC24D21172002C67BA /* IterableAPICallTaskProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPICallTaskProcessor.swift; sourceTree = ""; }; + ACC362BE24D21192002C67BA /* IterableTaskProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskProcessor.swift; sourceTree = ""; }; + ACC362C024D21272002C67BA /* IterableTaskResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskResult.swift; sourceTree = ""; }; + ACC362C224D21332002C67BA /* IterableTaskError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskError.swift; sourceTree = ""; }; + ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskProcessorTests.swift; sourceTree = ""; }; ACC6A84E2323910D003CC4BE /* UITestsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsHelper.swift; sourceTree = ""; }; ACC6A851232407B5003CC4BE /* InboxUITestsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxUITestsHelper.swift; sourceTree = ""; }; ACC87763215C20B50097E29B /* ui-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ui-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; ACC87765215C20B50097E29B /* UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITests.swift; sourceTree = ""; }; ACC87767215C20B50097E29B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProcessorTests.swift; sourceTree = ""; }; ACD6116B2107D004003E7F6B /* NetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = ""; }; ACD6116D21080564003E7F6B /* IterableAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPITests.swift; sourceTree = ""; }; ACDA975923159C36004C412E /* inbox-ui-tests-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "inbox-ui-tests-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -447,6 +524,10 @@ ACED4C00213F50B30055A497 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; ACEDF41C2183C2EC000B9BFE /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = ""; }; ACEDF41E2183C436000B9BFE /* PromiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseTests.swift; sourceTree = ""; }; + ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProcessorProtocol.swift; sourceTree = ""; }; + ACF40620250781F1005FD775 /* NetworkConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityManager.swift; sourceTree = ""; }; + ACF406222507BC72005FD775 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; + ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityManagerTests.swift; sourceTree = ""; }; ACF560D320E443BF000AAC23 /* host-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "host-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; ACF560D520E443BF000AAC23 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; ACF560DA20E443BF000AAC23 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -454,8 +535,12 @@ ACF560DF20E443C0000AAC23 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; ACF560E120E443C0000AAC23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; ACF560E720E55A6B000AAC23 /* IterableActionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableActionContext.swift; sourceTree = ""; }; + ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceHelper.swift; sourceTree = ""; }; + ACFD5AB824C8200C008E497A /* offline-events-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "offline-events-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + ACFD5ABC24C8200C008E497A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TasksCRUDTests.swift; sourceTree = ""; }; + ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskManagedObject.swift; sourceTree = ""; }; ACFF429E24656BDF00FDF10D /* ui-tests-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ui-tests-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - ACFF429F24656BDF00FDF10D /* host-app copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "host-app copy-Info.plist"; path = "/Users/tapash.majumder/work/iterable/mobile/ios/swift-sdk/host-app copy-Info.plist"; sourceTree = ""; }; ACFF42A324656CA100FDF10D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; ACFF42A624656D2600FDF10D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; ACFF42A824656D8E00FDF10D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -535,6 +620,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB524C8200C008E497A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ACFD5ABD24C8200C008E497A /* IterableSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF429224656BDF00FDF10D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -587,6 +680,14 @@ name = "Helper Files"; sourceTree = ""; }; + AC1AA1C724EBB39500F29C6B /* Notification Center */ = { + isa = PBXGroup; + children = ( + AC1AA1C824EBB3C300F29C6B /* IterableNotifications.swift */, + ); + name = "Notification Center"; + sourceTree = ""; + }; AC219C4A225FD7F900B98631 /* Inbox UI */ = { isa = PBXGroup; children = ( @@ -615,7 +716,6 @@ ACDA975A23159C37004C412E /* inbox-ui-tests-app */, ACFCA72920EB02DB00BFB277 /* tests */, 5550F22324217CFC0014456A /* misc */, - ACFF429F24656BDF00FDF10D /* host-app copy-Info.plist */, ); sourceTree = ""; }; @@ -632,6 +732,7 @@ ACDA976C23159C39004C412E /* inbox-ui-tests-app-ui-tests.xctest */, ACFF429E24656BDF00FDF10D /* ui-tests-app.app */, AC28480724AA44C600C1FC7F /* endpoint-tests.xctest */, + ACFD5AB824C8200C008E497A /* offline-events-tests.xctest */, ); name = Products; path = ../..; @@ -741,10 +842,35 @@ children = ( AC219C522260006600B98631 /* Assets.xcassets */, AC219C4F225FEDBD00B98631 /* SampleInboxCell.xib */, + AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */, ); path = Resources; sourceTree = ""; }; + AC50865124C60133001DC132 /* Persistence */ = { + isa = PBXGroup; + children = ( + AC50865524C603AC001DC132 /* IterablePersistence.swift */, + AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */, + AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */, + ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */, + ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */, + ); + name = Persistence; + sourceTree = ""; + }; + AC5E888724E1B7AD00752321 /* Request Processing */ = { + isa = PBXGroup; + children = ( + ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */, + AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */, + AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, + AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */, + AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */, + ); + name = "Request Processing"; + sourceTree = ""; + }; AC72A0AC20CF4C08004D7997 /* Util */ = { isa = PBXGroup; children = ( @@ -760,6 +886,11 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + ACF4061F25078186005FD775 /* Network */, + AC1AA1C724EBB39500F29C6B /* Notification Center */, + AC5E888724E1B7AD00752321 /* Request Processing */, + ACC362BB24D21153002C67BA /* Task Processing */, + AC50865124C60133001DC132 /* Persistence */, AC845105228DF5360052BB8F /* API Client */, AC0248062279132400495FB9 /* Dwifft */, AC426CC5211B5527002EDBE8 /* Fp */, @@ -776,7 +907,6 @@ AC72A0C520CF4CB9004D7997 /* IterableAPIInternal.swift */, AC2C668120D32F2800D46CC9 /* IterableAppIntegrationInternal.swift */, AC72A0BD20CF4C98004D7997 /* IterableDeepLinkManager.swift */, - ACD6116B2107D004003E7F6B /* NetworkHelper.swift */, AC2B79F621E6A38900A59080 /* NotificationHelper.swift */, ACEDF41C2183C2EC000B9BFE /* Promise.swift */, ); @@ -820,6 +950,7 @@ AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */, 5585DF8E22A73390000A32B9 /* IterableInboxViewControllerTests.swift */, AC2C667F20D31B1F00D46CC9 /* IterableNotificationResponseTests.swift */, + ACC362B724D17005002C67BA /* IterableRequestTests.swift */, AC776DA3211A17C700C27C27 /* IterableRequestUtilTests.swift */, ACE34AB4213776CB00691224 /* LocalStorageTests.swift */, ACED4C00213F50B30055A497 /* LoggingTests.swift */, @@ -859,6 +990,7 @@ 55B9F15224B6625D00E8198A /* ApiClientProtocol.swift */, AC8E9267246284F800BEB68E /* DataFieldsHelper.swift */, AC84510822910A0C0052BB8F /* RequestCreator.swift */, + ACC362B524D16D91002C67BA /* IterableRequest.swift */, ); name = "API Client"; sourceTree = ""; @@ -914,6 +1046,21 @@ path = common; sourceTree = ""; }; + ACC362BB24D21153002C67BA /* Task Processing */ = { + isa = PBXGroup; + children = ( + ACC362B924D20BBB002C67BA /* IterableAPICallRequest.swift */, + ACC362BE24D21192002C67BA /* IterableTaskProcessor.swift */, + ACC362BC24D21172002C67BA /* IterableAPICallTaskProcessor.swift */, + AC50865724C60426001DC132 /* IterableTask.swift */, + ACC362C024D21272002C67BA /* IterableTaskResult.swift */, + ACC362C224D21332002C67BA /* IterableTaskError.swift */, + AC1AA1C524EBB2DC00F29C6B /* IterableTaskRunner.swift */, + AC2AED4324EBC905000EE5F3 /* IterableTaskScheduler.swift */, + ); + name = "Task Processing"; + sourceTree = ""; + }; ACC87764215C20B50097E29B /* ui-tests */ = { isa = PBXGroup; children = ( @@ -963,6 +1110,17 @@ name = "Local Storage"; sourceTree = ""; }; + ACF4061F25078186005FD775 /* Network */ = { + isa = PBXGroup; + children = ( + ACD6116B2107D004003E7F6B /* NetworkHelper.swift */, + AC978D3D24FF953C00372B8C /* NetworkConnectivityChecker.swift */, + ACF40620250781F1005FD775 /* NetworkConnectivityManager.swift */, + ACF406222507BC72005FD775 /* NetworkMonitor.swift */, + ); + name = Network; + sourceTree = ""; + }; ACF560D420E443BF000AAC23 /* host-app */ = { isa = PBXGroup; children = ( @@ -985,10 +1143,26 @@ AC90C4D220D8632E00EECA5D /* notification-extension-tests */, AC7B142C20D02CE200877BFE /* swift-sdk-swift-tests */, ACC87764215C20B50097E29B /* ui-tests */, + ACFD5AB924C8200C008E497A /* offline-events-tests */, ); path = tests; sourceTree = ""; }; + ACFD5AB924C8200C008E497A /* offline-events-tests */ = { + isa = PBXGroup; + children = ( + ACFD5ABC24C8200C008E497A /* Info.plist */, + ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, + ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, + AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, + ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, + AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */, + ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */, + AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */, + ); + path = "offline-events-tests"; + sourceTree = ""; + }; ACFF42A224656C6200FDF10D /* ui-tests-app */ = { isa = PBXGroup; children = ( @@ -1205,6 +1379,25 @@ productReference = ACF560D320E443BF000AAC23 /* host-app.app */; productType = "com.apple.product-type.application"; }; + ACFD5AB724C8200C008E497A /* offline-events-tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = ACFD5AC224C8200C008E497A /* Build configuration list for PBXNativeTarget "offline-events-tests" */; + buildPhases = ( + ACFD5AB424C8200C008E497A /* Sources */, + ACFD5AB524C8200C008E497A /* Frameworks */, + ACFD5AB624C8200C008E497A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ACFD5ABF24C8200C008E497A /* PBXTargetDependency */, + ACFD5AC424C82013008E497A /* PBXTargetDependency */, + ); + name = "offline-events-tests"; + productName = "offline-events-tests"; + productReference = ACFD5AB824C8200C008E497A /* offline-events-tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; ACFF428924656BDF00FDF10D /* ui-tests-app */ = { isa = PBXNativeTarget; buildConfigurationList = ACFF429B24656BDF00FDF10D /* Build configuration list for PBXNativeTarget "ui-tests-app" */; @@ -1306,6 +1499,7 @@ ACDA975823159C36004C412E /* inbox-ui-tests-app */, ACDA976B23159C39004C412E /* inbox-ui-tests-app-ui-tests */, AC28480624AA44C600C1FC7F /* endpoint-tests */, + ACFD5AB724C8200C008E497A /* offline-events-tests */, ); }; /* End PBXProject section */ @@ -1390,6 +1584,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB624C8200C008E497A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF429424656BDF00FDF10D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1409,39 +1610,53 @@ files = ( AC31B042232AB53500BE25EB /* InboxImpressionTracker.swift in Sources */, 55D54656239AE5750093ED1E /* LoggingInternal.swift in Sources */, + ACF32BDB24E3EA7C0072E2CC /* RequestProcessorProtocol.swift in Sources */, AC426CC4211B5497002EDBE8 /* ServerResponse.swift in Sources */, AC219C49225FD7EB00B98631 /* IterableInboxViewController.swift in Sources */, AC3DD9C82142F3650046F886 /* ClassExtensions.swift in Sources */, AC219C50225FEDBD00B98631 /* IterableInboxCell.swift in Sources */, ACE6888D2228B86C00A95E5E /* InAppInternal.swift in Sources */, + AC1AA1C924EBB3C300F29C6B /* IterableNotifications.swift in Sources */, + ACFD5AB324C8179D008E497A /* PersistenceHelper.swift in Sources */, AC72A0CD20CF4CE2004D7997 /* IterableAppIntegration.swift in Sources */, AC72A0CE20CF4CE2004D7997 /* IterableAttributionInfo.swift in Sources */, + AC50865424C60172001DC132 /* IterableDataModel.xcdatamodeld in Sources */, ACA8D1A321910C66001B1332 /* IterableMessaging.swift in Sources */, + ACF40621250781F1005FD775 /* NetworkConnectivityManager.swift in Sources */, AC4B039622A8743F0043185B /* InAppManager+Functions.swift in Sources */, AC72A0D420CF4D19004D7997 /* IterableDeepLinkManager.swift in Sources */, ACC51A6B22A879070095E81F /* EmptyInAppManager.swift in Sources */, + ACC362BF24D21192002C67BA /* IterableTaskProcessor.swift in Sources */, AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */, AC72A0C920CF4CE2004D7997 /* IterableAction.swift in Sources */, AC72A0CA20CF4CE2004D7997 /* IterableActionRunner.swift in Sources */, ACD6116C2107D004003E7F6B /* NetworkHelper.swift in Sources */, + ACC362B624D16D91002C67BA /* IterableRequest.swift in Sources */, + ACC362BD24D21172002C67BA /* IterableAPICallTaskProcessor.swift in Sources */, AC84510922910A0C0052BB8F /* RequestCreator.swift in Sources */, ACB8273F22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift in Sources */, + AC5812F624F3A90F007E6D36 /* OfflineRequestProcessor.swift in Sources */, AC81918A22713A400014955E /* AbstractDiffCalculator.swift in Sources */, AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */, AC72A0D220CF4D12004D7997 /* IterableUtil.swift in Sources */, AC32E16821DD55B900BD4F83 /* OrderedDictionary.swift in Sources */, + ACF406232507BC72005FD775 /* NetworkMonitor.swift in Sources */, AC1712892416AEF400F2BB0E /* WebViewProtocol.swift in Sources */, AC3C10F9213F46A900A9B839 /* IterableLogging.swift in Sources */, ACE34AB72139D70B00691224 /* LocalStorageProtocol.swift in Sources */, AC819186227139230014955E /* SectionedValues.swift in Sources */, AC6FDD8820F4372E005D811E /* IterableAPI.swift in Sources */, ACF560E820E55A6B000AAC23 /* IterableActionContext.swift in Sources */, + AC5812F824F3AE8D007E6D36 /* RequestProcessor.swift in Sources */, AC2C668220D32F2800D46CC9 /* IterableAppIntegrationInternal.swift in Sources */, AC1BED9523F1D4C700FDD75F /* MiscInboxClasses.swift in Sources */, 556FB1EA244FAF6A00EDF6BD /* InAppPresenter.swift in Sources */, + AC2AED4424EBC905000EE5F3 /* IterableTaskScheduler.swift in Sources */, 55B9F15324B6625D00E8198A /* ApiClientProtocol.swift in Sources */, AC219C4D225FE4C000B98631 /* InboxMessageViewModel.swift in Sources */, + AC50865A24C60572001DC132 /* IterableCoreDataPersistence.swift in Sources */, AC72A0C820CF4CE2004D7997 /* Constants.swift in Sources */, + AC50865824C60426001DC132 /* IterableTask.swift in Sources */, AC2B79F721E6A38900A59080 /* NotificationHelper.swift in Sources */, AC4095A422B18B9D006EF67C /* InboxViewControllerViewModel.swift in Sources */, AC7A5261227BB9D10064D67E /* DependencyContainer.swift in Sources */, @@ -1455,6 +1670,8 @@ AC845107228DF54E0052BB8F /* ApiClient.swift in Sources */, AC72A0D120CF4D0B004D7997 /* InAppHelper.swift in Sources */, AC8874AA22178BD80075B54B /* InAppContentParser.swift in Sources */, + AC50865624C603AC001DC132 /* IterablePersistence.swift in Sources */, + ACC362BA24D20BBB002C67BA /* IterableAPICallRequest.swift in Sources */, AC776DA22118B86600C27C27 /* DeviceInfo.swift in Sources */, 55B3119B251015CF0056E4FC /* AuthManager.swift in Sources */, AC72A0CB20CF4CE2004D7997 /* IterableAPIInternal.swift in Sources */, @@ -1462,11 +1679,19 @@ AC31B040232AB42100BE25EB /* InboxSessionManager.swift in Sources */, ACE34AB321376B1000691224 /* UserDefaultsLocalStorage.swift in Sources */, AC8E9268246284F800BEB68E /* DataFieldsHelper.swift in Sources */, + AC5E888924E1B7CE00752321 /* OnlineRequestProcessor.swift in Sources */, + AC978D3E24FF953C00372B8C /* NetworkConnectivityChecker.swift in Sources */, + ACC362C324D21332002C67BA /* IterableTaskError.swift in Sources */, + ACC362C124D21272002C67BA /* IterableTaskResult.swift in Sources */, AC2C667E20D3111900D46CC9 /* DateProvider.swift in Sources */, ACA8D1AB21966555001B1332 /* InAppManager.swift in Sources */, AC81918822713A110014955E /* Dwifft+UIKit.swift in Sources */, AC426226238C27DD00164121 /* IterableInboxCell+Layout.swift in Sources */, AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */, + AC3A336D24F65579008225BA /* RequestProcessorUtil.swift in Sources */, + AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */, + AC1AA1C624EBB2DC00F29C6B /* IterableTaskRunner.swift in Sources */, + ACFD5AC824C8290E008E497A /* IterableTaskManagedObject.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1490,6 +1715,7 @@ buildActionMask = 2147483647; files = ( ACA8D1A62196309C001B1332 /* Common.swift in Sources */, + ACC362B824D17005002C67BA /* IterableRequestTests.swift in Sources */, AC2C668720D3435700D46CC9 /* IterableActionRunnerTests.swift in Sources */, 00CB31B621096129004ACDEC /* TestUtils.swift in Sources */, 55E6F460238E066400808BCE /* DeferredDeepLinkTests.swift in Sources */, @@ -1605,6 +1831,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB424C8200C008E497A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC3EFFF02510B8FB007F1330 /* TaskSchedulerTests.swift in Sources */, + AC67AF982507481200C1E974 /* NetworkConnectivityCheckerTests.swift in Sources */, + ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, + ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, + ACF406252507F90F005FD775 /* NetworkConnectivityManagerTests.swift in Sources */, + ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */, + ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */, + ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, + AC241C2224F5757C00F8F9CC /* Mocks.swift in Sources */, + ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, + ACC362C524D2C190002C67BA /* TaskProcessorTests.swift in Sources */, + AC2AED4224EBC60C000EE5F3 /* TaskRunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF428C24656BDF00FDF10D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1660,6 +1905,16 @@ target = ACF560D220E443BF000AAC23 /* host-app */; targetProxy = ACFCA72320EAB2B900BFB277 /* PBXContainerItemProxy */; }; + ACFD5ABF24C8200C008E497A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AC2263DE20CF49B8009800EB /* swift-sdk */; + targetProxy = ACFD5ABE24C8200C008E497A /* PBXContainerItemProxy */; + }; + ACFD5AC424C82013008E497A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ACF560D220E443BF000AAC23 /* host-app */; + targetProxy = ACFD5AC324C82013008E497A /* PBXContainerItemProxy */; + }; ACFF428A24656BDF00FDF10D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AC2263DE20CF49B8009800EB /* swift-sdk */; @@ -1913,7 +2168,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = JZ52G3H3Z6; + DEVELOPMENT_TEAM = BP98Z28R86; INFOPLIST_FILE = "tests/endpoint-tests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( @@ -1934,7 +2189,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = JZ52G3H3Z6; + DEVELOPMENT_TEAM = BP98Z28R86; INFOPLIST_FILE = "tests/endpoint-tests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( @@ -2239,7 +2494,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "iterable.host-app"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "host-app"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -2260,8 +2515,51 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "iterable.host-app"; + PRODUCT_NAME = "host-app"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + ACFD5AC024C8200C008E497A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BP98Z28R86; + INFOPLIST_FILE = "tests/offline-events-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.iterable.offline-events-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/host-app.app/host-app"; + }; + name = Debug; + }; + ACFD5AC124C8200C008E497A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BP98Z28R86; + INFOPLIST_FILE = "tests/offline-events-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.iterable.offline-events-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/host-app.app/host-app"; }; name = Release; }; @@ -2281,7 +2579,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "iterable.ui-tests-app"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "ui-tests-app"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -2302,7 +2600,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "iterable.ui-tests-app"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "ui-tests-app"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -2400,6 +2698,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + ACFD5AC224C8200C008E497A /* Build configuration list for PBXNativeTarget "offline-events-tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ACFD5AC024C8200C008E497A /* Debug */, + ACFD5AC124C8200C008E497A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; ACFF429B24656BDF00FDF10D /* Build configuration list for PBXNativeTarget "ui-tests-app" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2429,6 +2736,19 @@ productName = OHHTTPStubs; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + AC50865324C60172001DC132 /* IterableDataModel.xcdatamodel */, + ); + currentVersion = AC50865324C60172001DC132 /* IterableDataModel.xcdatamodel */; + path = IterableDataModel.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = AC2263D620CF49B8009800EB /* Project object */; } diff --git a/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme b/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme index 4c8bc650f..878afa34a 100644 --- a/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme +++ b/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme @@ -62,6 +62,20 @@ ReferencedContainer = "container:swift-sdk.xcodeproj"> + + + + + + + + RequestCreator { + private func createRequestCreator() -> Result { guard let authProvider = authProvider else { - fatalError("authProvider is missing") + return .failure(IterableError.general(description: "authProvider is missing")) } - return RequestCreator(apiKey: apiKey, auth: authProvider.auth, deviceMetadata: deviceMetadata) + return .success(RequestCreator(apiKey: apiKey, auth: authProvider.auth, deviceMetadata: deviceMetadata)) } + private func createIterableHeaders() -> [String: String] { var headers = [JsonKey.contentType.jsonKey: JsonValue.applicationJson.jsonStringValue, @@ -85,52 +86,53 @@ class ApiClient { // MARK: - API REQUEST CALLS extension ApiClient: ApiClientProtocol { - func register(hexToken: String, - appName: String, - deviceId: String, - sdkVersion: String?, - deviceAttributes: [String: String], - pushServicePlatform: String, - notificationsEnabled: Bool) -> Future { - send(iterableRequestResult: createRequestCreator().createRegisterTokenRequest(hexToken: hexToken, - appName: appName, - deviceId: deviceId, - sdkVersion: sdkVersion, - deviceAttributes: deviceAttributes, - pushServicePlatform: pushServicePlatform, - notificationsEnabled: notificationsEnabled)) + func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Future { + let result = createRequestCreator().flatMap { $0.createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, + notificationsEnabled: notificationsEnabled) } + return send(iterableRequestResult: result) } func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool) -> Future { - send(iterableRequestResult: createRequestCreator().createUpdateUserRequest(dataFields: dataFields, mergeNestedObjects: mergeNestedObjects)) + let result = createRequestCreator().flatMap { $0.createUpdateUserRequest(dataFields: dataFields, + mergeNestedObjects: mergeNestedObjects) } + return send(iterableRequestResult: result) } func updateEmail(newEmail: String) -> Future { - send(iterableRequestResult: createRequestCreator().createUpdateEmailRequest(newEmail: newEmail)) + let result = createRequestCreator().flatMap { $0.createUpdateEmailRequest(newEmail: newEmail) } + return send(iterableRequestResult: result) } func getInAppMessages(_ count: NSNumber) -> Future { - send(iterableRequestResult: createRequestCreator().createGetInAppMessagesRequest(count)) + let result = createRequestCreator().flatMap { $0.createGetInAppMessagesRequest(count) } + return send(iterableRequestResult: result) } func disableDevice(forAllUsers allUsers: Bool, hexToken: String) -> Future { - send(iterableRequestResult: createRequestCreator().createDisableDeviceRequest(forAllUsers: allUsers, hexToken: hexToken)) + let result = createRequestCreator().flatMap { $0.createDisableDeviceRequest(forAllUsers: allUsers, + hexToken: hexToken) } + return send(iterableRequestResult: result) } func track(purchase total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]?) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackPurchaseRequest(total, items: items, dataFields: dataFields)) + let result = createRequestCreator().flatMap { $0.createTrackPurchaseRequest(total, items: items, + dataFields: dataFields) } + return send(iterableRequestResult: result) } func track(pushOpen campaignId: NSNumber, templateId: NSNumber?, messageId: String, appAlreadyRunning: Bool, dataFields: [AnyHashable: Any]?) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackPushOpenRequest(campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) + let result = createRequestCreator().flatMap { $0.createTrackPushOpenRequest(campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields) } + return send(iterableRequestResult: result) } func track(event eventName: String, dataFields: [AnyHashable: Any]?) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackEventRequest(eventName, dataFields: dataFields)) + let result = createRequestCreator().flatMap { $0.createTrackEventRequest(eventName, + dataFields: dataFields) } + return send(iterableRequestResult: result) } func updateSubscriptions(_ emailListIds: [NSNumber]? = nil, @@ -139,40 +141,52 @@ extension ApiClient: ApiClientProtocol { subscribedMessageTypeIds: [NSNumber]? = nil, campaignId: NSNumber? = nil, templateId: NSNumber? = nil) -> Future { - send(iterableRequestResult: createRequestCreator().createUpdateSubscriptionsRequest(emailListIds, - unsubscribedChannelIds: unsubscribedChannelIds, - unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, - subscribedMessageTypeIds: subscribedMessageTypeIds, - campaignId: campaignId, - templateId: templateId)) + let result = createRequestCreator().flatMap { $0.createUpdateSubscriptionsRequest(emailListIds, + unsubscribedChannelIds: unsubscribedChannelIds, + unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, + subscribedMessageTypeIds: subscribedMessageTypeIds, + campaignId: campaignId, + templateId: templateId) } + return send(iterableRequestResult: result) } func track(inAppOpen inAppMessageContext: InAppMessageContext) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppOpenRequest(inAppMessageContext: inAppMessageContext)) + let result = createRequestCreator().flatMap { $0.createTrackInAppOpenRequest(inAppMessageContext: inAppMessageContext) } + return send(iterableRequestResult: result) } func track(inAppClick inAppMessageContext: InAppMessageContext, clickedUrl: String) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppClickRequest(inAppMessageContext: inAppMessageContext, clickedUrl: clickedUrl)) + let result = createRequestCreator().flatMap { $0.createTrackInAppClickRequest(inAppMessageContext: inAppMessageContext, + clickedUrl: clickedUrl) } + return send(iterableRequestResult: result) } func track(inAppClose inAppMessageContext: InAppMessageContext, source: InAppCloseSource?, clickedUrl: String?) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppCloseRequest(inAppMessageContext: inAppMessageContext, source: source, clickedUrl: clickedUrl)) + let result = createRequestCreator().flatMap { $0.createTrackInAppCloseRequest(inAppMessageContext: inAppMessageContext, + source: source, + clickedUrl: clickedUrl) } + return send(iterableRequestResult: result) } func track(inAppDelivery inAppMessageContext: InAppMessageContext) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppDeliveryRequest(inAppMessageContext: inAppMessageContext)) + let result = createRequestCreator().flatMap { $0.createTrackInAppDeliveryRequest(inAppMessageContext: inAppMessageContext) } + return send(iterableRequestResult: result) } func track(inboxSession: IterableInboxSession) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInboxSessionRequest(inboxSession: inboxSession)) + let result = createRequestCreator().flatMap { $0.createTrackInboxSessionRequest(inboxSession: inboxSession) } + return send(iterableRequestResult: result) } func inAppConsume(messageId: String) -> Future { - send(iterableRequestResult: createRequestCreator().createInAppConsumeRequest(messageId)) + let result = createRequestCreator().flatMap { $0.createInAppConsumeRequest(messageId) } + return send(iterableRequestResult: result) } func inAppConsume(inAppMessageContext: InAppMessageContext, source: InAppDeleteSource?) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppConsumeRequest(inAppMessageContext: inAppMessageContext, source: source)) + let result = createRequestCreator().flatMap { $0.createTrackInAppConsumeRequest(inAppMessageContext: inAppMessageContext, + source: source) } + return send(iterableRequestResult: result) } } @@ -181,11 +195,14 @@ extension ApiClient: ApiClientProtocol { extension ApiClient { // deprecated - will be removed in version 6.3.x or above func track(inAppOpen messageId: String) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppOpenRequest(messageId)) + let value = createRequestCreator().flatMap { $0.createTrackInAppOpenRequest(messageId) } + return send(iterableRequestResult: value) } // deprecated - will be removed in version 6.3.x or above func track(inAppClick messageId: String, clickedUrl: String) -> Future { - send(iterableRequestResult: createRequestCreator().createTrackInAppClickRequest(messageId, clickedUrl: clickedUrl)) + let result = createRequestCreator().flatMap { $0.createTrackInAppClickRequest(messageId, + clickedUrl: clickedUrl) } + return send(iterableRequestResult: result) } } diff --git a/swift-sdk/Internal/ApiClientProtocol.swift b/swift-sdk/Internal/ApiClientProtocol.swift index 92c9225da..e3a3a8548 100644 --- a/swift-sdk/Internal/ApiClientProtocol.swift +++ b/swift-sdk/Internal/ApiClientProtocol.swift @@ -6,13 +6,7 @@ import Foundation protocol ApiClientProtocol: AnyObject { - func register(hexToken: String, - appName: String, - deviceId: String, - sdkVersion: String?, - deviceAttributes: [String: String], - pushServicePlatform: String, - notificationsEnabled: Bool) -> Future + func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Future func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool) -> Future diff --git a/swift-sdk/Internal/Auth.swift b/swift-sdk/Internal/Auth.swift index 86539d900..faaf182a4 100644 --- a/swift-sdk/Internal/Auth.swift +++ b/swift-sdk/Internal/Auth.swift @@ -30,3 +30,5 @@ struct Auth { case none } } + +extension Auth: Codable {} diff --git a/swift-sdk/Internal/ClassExtensions.swift b/swift-sdk/Internal/ClassExtensions.swift index 515f8d7be..b17e00ba8 100644 --- a/swift-sdk/Internal/ClassExtensions.swift +++ b/swift-sdk/Internal/ClassExtensions.swift @@ -14,6 +14,16 @@ extension Array { } } +extension Array where Element: Comparable { + func isAscending() -> Bool { + return zip(self, self.dropFirst()).allSatisfy(<=) + } + + func isDescending() -> Bool { + return zip(self, self.dropFirst()).allSatisfy(>=) + } +} + extension Dictionary where Key == AnyHashable, Value == Any { func getValue(for key: JsonKey) -> Any? { self[key.jsonKey] diff --git a/swift-sdk/Internal/CoreDataUtil.swift b/swift-sdk/Internal/CoreDataUtil.swift new file mode 100644 index 000000000..e0e8891fd --- /dev/null +++ b/swift-sdk/Internal/CoreDataUtil.swift @@ -0,0 +1,68 @@ +// +// Created by Tapash Majumder on 7/21/20. +// Copyright © 2020 Iterable. All rights reserved. +// +// This file should contain general CoreData helper methods. +// This should not be dependent on Iterable classes. + +import CoreData +import Foundation + +struct CoreDataUtil { + static func create(context: NSManagedObjectContext, entity: String) -> T? { + NSEntityDescription.insertNewObject(forEntityName: entity, into: context) as? T + } + + static func findEntitiyByColumn(context: NSManagedObjectContext, + entity: String, + columnName: String, + columnValue: Any) throws -> T? { + try findEntitiesByColumns(context: context, entity: entity, columns: [columnName: columnValue]).first + } + + static func findAll(context: NSManagedObjectContext, entity: String) throws -> [T] { + let request = NSFetchRequest(entityName: entity) + return try context.fetch(request) + } + + static func findEntitiesByColumns(context: NSManagedObjectContext, entity: String, columns: [String: Any]) throws -> [T] { + let request = NSFetchRequest(entityName: entity) + request.predicate = createColumnsPredicate(columns: columns) + return try context.fetch(request) + } + + static func findSortedEntities(context: NSManagedObjectContext, + entity: String, + column: String, + ascending: Bool, + limit: Int) throws -> [T] { + + let sortDescriptor = NSSortDescriptor(key: column, ascending: ascending) + let request = NSFetchRequest(entityName: entity) + request.sortDescriptors = [sortDescriptor] + request.fetchLimit = limit + + return try context.fetch(request) + } + + private static func createColumnsPredicate(columns: [String: Any]) -> NSPredicate { + var subPredicates = [NSPredicate]() + for (columnName, columnValue) in columns { + subPredicates.append(createColumnPredicate(columnName: columnName, columnValue: columnValue)) + } + + return NSCompoundPredicate(andPredicateWithSubpredicates: subPredicates) + } + + private static func createColumnPredicate(columnName: String, columnValue: Any) -> NSPredicate { + if let stringValue = columnValue as? String { + return NSPredicate(format: "%K ==[c] %@", columnName, stringValue) + } else if let intValue = columnValue as? Int { + return NSPredicate(format: "%K == %d", columnName, intValue) + } else if let boolValue = columnValue as? Bool { + return NSPredicate(format: "%K == %@", columnName, NSNumber(value: boolValue)) + } else { + fatalError("unsuppored value: \(columnValue)") + } + } +} diff --git a/swift-sdk/Internal/DataFieldsHelper.swift b/swift-sdk/Internal/DataFieldsHelper.swift index c04305f8e..381c48071 100644 --- a/swift-sdk/Internal/DataFieldsHelper.swift +++ b/swift-sdk/Internal/DataFieldsHelper.swift @@ -60,7 +60,7 @@ struct DataFieldsHelper { fields[JsonKey.Device.systemName] = device.systemName fields[JsonKey.Device.systemVersion] = device.systemVersion fields[JsonKey.Device.model] = device.model - + if let identifierForVendor = device.identifierForVendor?.uuidString { fields[JsonKey.Device.vendorId] = identifierForVendor } diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index df69bc09e..d7a1c2d82 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -20,6 +20,7 @@ protocol DependencyContainerProtocol { var apnsTypeChecker: APNSTypeCheckerProtocol { get } func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol + func createPersistenceContextProvider() -> IterablePersistenceContextProvider? } extension DependencyContainerProtocol { @@ -47,6 +48,66 @@ extension DependencyContainerProtocol { localStorage: localStorage, dateProvider: dateProvider) } + + func createRequestProcessor(apiKey: String, + config: IterableConfig, + authProvider: AuthProvider?, + authManager: IterableInternalAuthManagerProtocol, + deviceMetadata: DeviceMetadata) -> RequestProcessorProtocol { + if #available(iOS 10.0, *) { + return RequestProcessor(onlineCreator: { [weak authProvider] in + OnlineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) }, + offlineCreator: { [weak authProvider] in + guard let persistenceContextProvider = createPersistenceContextProvider() else { + return nil + } + + return OfflineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + deviceMetadata: deviceMetadata, + taskScheduler: createTaskScheduler(persistenceContextProvider: persistenceContextProvider), + taskRunner: createTaskRunner(persistenceContextProvider: persistenceContextProvider), + notificationCenter: notificationCenter) }, + strategy: DefaultRequestProcessorStrategy(selectOffline: config.enableOfflineMode)) + } else { + return OnlineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } + } + + func createPersistenceContextProvider() -> IterablePersistenceContextProvider? { + if #available(iOS 10.0, *) { + return CoreDataPersistenceContextProvider(dateProvider: dateProvider) + } else { + fatalError("Unable to create persistence container for iOS < 10") + } + } + + @available(iOS 10.0, *) + private func createTaskScheduler(persistenceContextProvider: IterablePersistenceContextProvider) -> IterableTaskScheduler { + IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider) + } + + @available(iOS 10.0, *) + private func createTaskRunner(persistenceContextProvider: IterablePersistenceContextProvider) -> IterableTaskRunner { + IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + connectivityManager: NetworkConnectivityManager()) + } } struct DependencyContainer: DependencyContainerProtocol { diff --git a/swift-sdk/Internal/InAppInternal.swift b/swift-sdk/Internal/InAppInternal.swift index 6e606d0a7..2eb3b0c59 100644 --- a/swift-sdk/Internal/InAppInternal.swift +++ b/swift-sdk/Internal/InAppInternal.swift @@ -10,7 +10,7 @@ protocol InAppFetcherProtocol { } /// For callbacks when silent push notifications arrive -protocol InAppNotifiable { +protocol InAppNotifiable: AnyObject { func scheduleSync() -> Future func onInAppRemoved(messageId: String) func reset() -> Future diff --git a/swift-sdk/Internal/InAppManager.swift b/swift-sdk/Internal/InAppManager.swift index 8fc017d92..80bb9939d 100644 --- a/swift-sdk/Internal/InAppManager.swift +++ b/swift-sdk/Internal/InAppManager.swift @@ -6,14 +6,6 @@ import Foundation import UIKit -protocol NotificationCenterProtocol { - func addObserver(_ observer: Any, selector: Selector, name: Notification.Name?, object: Any?) - func removeObserver(_ observer: Any) - func post(name: Notification.Name, object: Any?, userInfo: [AnyHashable: Any]?) -} - -extension NotificationCenter: NotificationCenterProtocol {} - protocol InAppDisplayChecker { func isOkToShowNow(message: IterableInAppMessage) -> Bool } diff --git a/swift-sdk/Internal/IterableAPICallRequest.swift b/swift-sdk/Internal/IterableAPICallRequest.swift new file mode 100644 index 000000000..1674a234e --- /dev/null +++ b/swift-sdk/Internal/IterableAPICallRequest.swift @@ -0,0 +1,49 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +/// This struct encapsulates all the data that needs to be sent to Iterable backend. +/// This struct must be `Codable`. +struct IterableAPICallRequest { + let apiKey: String + let endPoint: String + let auth: Auth + let deviceMetadata: DeviceMetadata + let iterableRequest: IterableRequest + + func convertToURLRequest() -> URLRequest? { + switch iterableRequest { + case let .get(getRequest): + return IterableRequestUtil.createGetRequest(forApiEndPoint: endPoint, path: getRequest.path, headers: createIterableHeaders(), args: getRequest.args) + case let .post(postRequest): + return IterableRequestUtil.createPostRequest(forApiEndPoint: endPoint, path: postRequest.path, headers: createIterableHeaders(), args: postRequest.args, body: postRequest.body) + } + } + + func getPath() -> String { + switch iterableRequest { + case .get(let request): + return request.path + case .post(let request): + return request.path + } + } + + private func createIterableHeaders() -> [String: String] { + var headers = [JsonKey.contentType.jsonKey: JsonValue.applicationJson.jsonStringValue, + JsonKey.Header.sdkPlatform: JsonValue.iOS.jsonStringValue, + JsonKey.Header.sdkVersion: IterableAPI.sdkVersion, + JsonKey.Header.apiKey: apiKey] + + if let authToken = auth.authToken { + headers[JsonKey.Header.authorization] = "Bearer \(authToken)" + } + + return headers + } +} + +extension IterableAPICallRequest: Codable {} diff --git a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift new file mode 100644 index 000000000..eaf7b2271 --- /dev/null +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -0,0 +1,48 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct IterableAPICallTaskProcessor: IterableTaskProcessor { + let networkSession: NetworkSessionProtocol + + func process(task: IterableTask) throws -> Future { + ITBInfo() + guard let data = task.data else { + return IterableTaskError.createErroredFuture(reason: "expecting data") + } + + let iterableRequest = try JSONDecoder().decode(IterableAPICallRequest.self, from: data) + guard let urlRequest = iterableRequest.convertToURLRequest() else { + return IterableTaskError.createErroredFuture(reason: "could not convert to url request") + } + + let result = Promise() + NetworkHelper.sendRequest(urlRequest, usingSession: networkSession) + .onSuccess { sendRequestValue in + ITBInfo("Task finished successfully") + result.resolve(with: .success(detail: sendRequestValue)) + } + .onError { sendRequestError in + if IterableAPICallTaskProcessor.isNetworkUnavailable(sendRequestError: sendRequestError) { + ITBInfo("Network is unavailable") + result.resolve(with: .failureWithRetry(retryAfter: nil, detail: sendRequestError)) + } else { + ITBInfo("Unrecoverable error") + result.resolve(with: .failureWithNoRetry(detail: sendRequestError)) + } + } + + return result + } + + private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool { + if let originalError = sendRequestError.originalError { + return originalError.localizedDescription.lowercased().contains("offline") + } else { + return false + } + } +} diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 3f646aa31..f656231b8 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -91,29 +91,6 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deviceAttributes.removeValue(forKey: name) } - static func defaultOnSuccess(_ identifier: String) -> OnSuccessHandler { - { data in - if let data = data { - ITBInfo("\(identifier) succeeded, got response: \(data)") - } else { - ITBInfo("\(identifier) succeeded.") - } - } - } - - static func defaultOnFailure(_ identifier: String) -> OnFailureHandler { - { reason, data in - var toLog = "\(identifier) failed:" - if let reason = reason { - toLog += ", \(reason)" - } - if let data = data { - toLog += ", got response \(String(data: data, encoding: .utf8) ?? "nil")" - } - ITBError(toLog) - } - } - func setEmail(_ email: String?) { if email != _email { logoutPreviousUser() @@ -154,148 +131,165 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { // MARK: - API Request Calls + @discardableResult func register(token: Data, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("registerToken"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("registerToken")) { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { guard let appName = pushIntegrationName else { - ITBError("registerToken: appName is nil") - onFailure?("Not registering device token - appName must not be nil", nil) - return + let errorMessage = "Not registering device token - appName must not be nil" + ITBError(errorMessage) + onFailure?(errorMessage, nil) + return SendRequestError.createErroredFuture(reason: errorMessage) } - self.register(token: token, - appName: appName, - pushServicePlatform: self.config.pushPlatform, - notificationsEnabled: notificationStateProvider.notificationsEnabled, - onSuccess: onSuccess, - onFailure: onFailure) + hexToken = token.hexString() + let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(), + appName: appName, + pushServicePlatform: config.pushPlatform, + apnsType: dependencyContainer.apnsTypeChecker.apnsType, + deviceId: deviceId, + deviceAttributes: deviceAttributes, + sdkVersion: localStorage.sdkVersion) + return requestProcessor.register(registerTokenInfo: registerTokenInfo, + notificationStateProvider: notificationStateProvider, + onSuccess: onSuccess, + onFailure: onFailure) } - func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("disableDevice"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("disableDevice")) { - disableDevice(forAllUsers: false, onSuccess: onSuccess, onFailure: onFailure) + @discardableResult + func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + guard let hexToken = hexToken else { + let errorMessage = "no token present" + onFailure?(errorMessage, nil) + return SendRequestError.createErroredFuture(reason: errorMessage) + } + guard userId != nil || email != nil else { + let errorMessage = "either userId or email must be present" + onFailure?(errorMessage, nil) + return SendRequestError.createErroredFuture(reason: errorMessage) + } + + return requestProcessor.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure) } - func disableDeviceForAllUsers(withOnSuccess onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("disableDevice"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("disableDevice")) { - disableDevice(forAllUsers: true, onSuccess: onSuccess, onFailure: onFailure) + @discardableResult + func disableDeviceForAllUsers(withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + guard let hexToken = hexToken else { + let errorMessage = "no token present" + onFailure?(errorMessage, nil) + return SendRequestError.createErroredFuture(reason: errorMessage) + } + return requestProcessor.disableDeviceForAllUsers(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure) } + @discardableResult func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("updateUser"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("updateUser")) { - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects, onSuccess: onSuccess, onFailure: onFailure) } + @discardableResult func updateEmail(_ newEmail: String, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("updateEmail"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("updateEmail")) { - IterableAPIInternal.call( - successHandler: { json in - if self.email != nil { - self.setEmail(newEmail) - } - - onSuccess?(json) - }, - andFailureHandler: { reason, data in - onFailure?(reason, data) - }, - andAuthManager: authManager, - forResult: apiClient.updateEmail(newEmail: newEmail)) + withToken token: String? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.updateEmail(newEmail, onSuccess: nil, onFailure: nil).onSuccess { json in + if self.email != nil { + self.setEmail(newEmail) + } + onSuccess?(json) + }.onError { error in + onFailure?(error.reason, error.data) + } } + @discardableResult func trackPurchase(_ total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackPurchase"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackPurchase")) { - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackPurchase(total, items: items, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) } + @discardableResult func trackPushOpen(_ userInfo: [AnyHashable: Any], dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackPushOpen"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackPushOpen")) { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { save(pushPayload: userInfo) if let metadata = IterablePushNotificationMetadata.metadata(fromLaunchOptions: userInfo), metadata.isRealCampaignNotification() { - trackPushOpen(metadata.campaignId, - templateId: metadata.templateId, - messageId: metadata.messageId, - appAlreadyRunning: false, - dataFields: dataFields, - onSuccess: onSuccess, - onFailure: onFailure) + return trackPushOpen(metadata.campaignId, + templateId: metadata.templateId, + messageId: metadata.messageId, + appAlreadyRunning: false, + dataFields: dataFields, + onSuccess: onSuccess, + onFailure: onFailure) } else { - onFailure?("Not tracking push open - payload is not an Iterable notification, or is a test/proof/ghost push", nil) + return SendRequestError.createErroredFuture(reason: "Not tracking push open - payload is not an Iterable notification, or is a test/proof/ghost push") } } + @discardableResult func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String, appAlreadyRunning: Bool, dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackPushOpen"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackPushOpen")) { - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackPushOpen(campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields, + onSuccess: onSuccess, + onFailure: onFailure) } + @discardableResult func track(_ eventName: String, dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackEvent"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackEvent")) { - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.track(event: eventName, dataFields: dataFields)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.track(event: eventName, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) } + @discardableResult func updateSubscriptions(_ emailListIds: [NSNumber]?, unsubscribedChannelIds: [NSNumber]?, unsubscribedMessageTypeIds: [NSNumber]?, subscribedMessageTypeIds: [NSNumber]?, campaignId: NSNumber?, templateId: NSNumber?, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("updateSubscriptions"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("updateSubscriptions")) { - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.updateSubscriptions(emailListIds, - unsubscribedChannelIds: unsubscribedChannelIds, - unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, - subscribedMessageTypeIds: subscribedMessageTypeIds, - campaignId: campaignId, - templateId: templateId)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let updateSubscriptionsInfo = UpdateSubscriptionsInfo(emailListIds: emailListIds, + unsubscribedChannelIds: unsubscribedChannelIds, + unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, + subscribedMessageTypeIds: subscribedMessageTypeIds, + campaignId: campaignId, + templateId: templateId) + return requestProcessor.updateSubscriptions(info: updateSubscriptionsInfo, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult func trackInAppOpen(_ message: IterableInAppMessage, location: InAppLocation, inboxSessionId: String? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackInAppOpen"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackInAppOpen")) -> Future { - let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) - return IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppOpen(message, + location: location, + inboxSessionId: inboxSessionId, + onSuccess: onSuccess, + onFailure: onFailure) } @discardableResult @@ -303,14 +297,13 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { location: InAppLocation = .inApp, inboxSessionId: String? = nil, clickedUrl: String, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackInAppClick"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackInAppClick")) -> Future { - let result = apiClient.track(inAppClick: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), - clickedUrl: clickedUrl) - return IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppClick(message, location: location, + inboxSessionId: inboxSessionId, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) } @discardableResult @@ -319,50 +312,49 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { inboxSessionId: String? = nil, source: InAppCloseSource? = nil, clickedUrl: String? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackInAppClose"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackInAppClose")) -> Future { - let result = apiClient.track(inAppClose: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), - source: source, - clickedUrl: clickedUrl) - return IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppClose(message, + location: location, + inboxSessionId: inboxSessionId, + source: source, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) } @discardableResult func track(inboxSession: IterableInboxSession, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("trackInboxSession"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("trackInboxSession")) -> Future { - let result = apiClient.track(inboxSession: inboxSession) - - return IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.track(inboxSession: inboxSession, onSuccess: onSuccess, onFailure: onFailure) } - func track(inAppDelivery message: IterableInAppMessage) { - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("trackInAppDelivery"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("trackInAppDelivery"), - andAuthManager: authManager, - forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.track(inAppDelivery: message, onSuccess: onSuccess, onFailure: onFailure) } - func inAppConsume(_ messageId: String) { - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("inAppConsume"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("inAppConsume"), - andAuthManager: authManager, - forResult: apiClient.inAppConsume(messageId: messageId)) + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.inAppConsume(messageId, onSuccess: onSuccess, onFailure: onFailure) } - func inAppConsume(message: IterableInAppMessage, location: InAppLocation = .inApp, source: InAppDeleteSource? = nil) { - let result = apiClient.inAppConsume(inAppMessageContext: InAppMessageContext.from(message: message, location: location), - source: source) - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("inAppConsumeWithSource"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("inAppConsumeWithSource"), - andAuthManager: authManager, - forResult: result) + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation = .inApp, + source: InAppDeleteSource? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.inAppConsume(message: message, + location: location, + source: source, + onSuccess: onSuccess, + onFailure: onFailure) } // MARK: - Private/Internal @@ -387,7 +379,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { private var launchOptions: [UIApplication.LaunchOptionsKey: Any]? - lazy var apiClient: ApiClient = { + lazy var apiClient: ApiClientProtocol = { ApiClient(apiKey: apiKey, authProvider: self, endPoint: config.apiEndpoint, @@ -395,6 +387,14 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deviceMetadata: deviceMetadata) }() + private lazy var requestProcessor: RequestProcessorProtocol = { + dependencyContainer.createRequestProcessor(apiKey: apiKey, + config: config, + authProvider: self, + authManager: authManager, + deviceMetadata: deviceMetadata) + }() + private var deviceAttributes = [String: String]() private var pushIntegrationName: String? { @@ -453,17 +453,6 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { _ = inAppManager.scheduleSync() } - private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { - switch pushServicePlatform { - case .production: - return JsonValue.apnsProduction.jsonStringValue - case .sandbox: - return JsonValue.apnsSandbox.jsonStringValue - case .auto: - return apnsType == .sandbox ? JsonValue.apnsSandbox.jsonStringValue : JsonValue.apnsProduction.jsonStringValue - } - } - private func storeIdentifierData() { localStorage.email = _email localStorage.userId = _userId @@ -474,29 +463,6 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { _userId = localStorage.userId } - @discardableResult - private func register(token: Data, - appName: String, - pushServicePlatform: PushServicePlatform, - notificationsEnabled: Bool, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("registerToken"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("registerToken")) -> Future { - hexToken = token.hexString() - - let pushServicePlatformString = IterableAPIInternal.pushServicePlatformToString(pushServicePlatform, apnsType: dependencyContainer.apnsTypeChecker.apnsType) - - return IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.register(hexToken: hexToken!, - appName: appName, - deviceId: deviceId, - sdkVersion: localStorage.sdkVersion, - deviceAttributes: deviceAttributes, - pushServicePlatform: pushServicePlatformString, - notificationsEnabled: notificationsEnabled)) - } - private func save(pushPayload payload: [AnyHashable: Any]) { let expiration = Calendar.current.date(byAdding: .hour, value: Const.UserDefaults.payloadExpiration, @@ -510,50 +476,6 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { } } - private func disableDevice(forAllUsers allUsers: Bool, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("disableDevice"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("disableDevice")) { - guard let hexToken = hexToken else { - ITBError("Device not registered.") - onFailure?("Device not registered.", nil) - return - } - - guard !(allUsers == false && email == nil && userId == nil) else { - ITBError("Emal or userId must be set.") - onFailure?("Email or userId must be set.", nil) - return - } - - IterableAPIInternal.call(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) - } - - @discardableResult - private static func call(successHandler onSuccess: OnSuccessHandler? = nil, - andFailureHandler onFailure: OnFailureHandler? = nil, - andAuthManager authManager: IterableInternalAuthManagerProtocol? = nil, - forResult result: Future - ) -> Future { - result.onSuccess { json in - authManager?.resetFailedAuthCount() - onSuccess?(json) - }.onError { error in - if error.httpStatusCode == 401, error.iterableCode == JsonValue.Code.invalidJwtPayload { - ITBError(error.reason) - authManager?.requestNewAuthToken(hasFailedPriorAuth: true, onSuccess: nil) - } else if error.httpStatusCode == 401, error.iterableCode == JsonValue.Code.badApiKey { - ITBError(error.reason) - } - - onFailure?(error.reason, error.data) - } - - return result - } - // package private method. Do not call this directly. init(apiKey: String, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil, @@ -596,6 +518,8 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { handle(launchOptions: launchOptions) + requestProcessor.start() + return inAppManager.start() } @@ -682,6 +606,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deinit { ITBInfo() + requestProcessor.stop() } } @@ -689,17 +614,20 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { extension IterableAPIInternal { // deprecated - will be removed in version 6.3.x or above - func trackInAppOpen(_ messageId: String) { - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("trackInAppOpen"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("trackInAppOpen"), - forResult: apiClient.track(inAppOpen: messageId)) + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppOpen(messageId, onSuccess: onSuccess, onFailure: onFailure) } // deprecated - will be removed in version 6.3.x or above - func trackInAppClick(_ messageId: String, clickedUrl: String) { - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("trackInAppClick"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("trackInAppClick"), - forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppClick(messageId, clickedUrl: clickedUrl, onSuccess: onSuccess, onFailure: onFailure) } // deprecated - will be removed in version 6.3.x or above diff --git a/swift-sdk/Internal/IterableAppIntegrationInternal.swift b/swift-sdk/Internal/IterableAppIntegrationInternal.swift index d9ec55cd4..107fa9267 100644 --- a/swift-sdk/Internal/IterableAppIntegrationInternal.swift +++ b/swift-sdk/Internal/IterableAppIntegrationInternal.swift @@ -78,21 +78,23 @@ struct UserNotificationResponse: NotificationResponseProtocol { } /// Abstraction of push tracking -public protocol PushTrackerProtocol: AnyObject { +protocol PushTrackerProtocol: AnyObject { var lastPushPayload: [AnyHashable: Any]? { get } + @discardableResult func trackPushOpen(_ userInfo: [AnyHashable: Any], dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) + onFailure: OnFailureHandler?) -> Future + @discardableResult func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String, appAlreadyRunning: Bool, dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) + onFailure: OnFailureHandler?) -> Future } extension PushTrackerProtocol { @@ -100,8 +102,8 @@ extension PushTrackerProtocol { dataFields: [AnyHashable: Any]? = nil) { trackPushOpen(userInfo, dataFields: dataFields, - onSuccess: IterableAPIInternal.defaultOnSuccess("trackPushOpen"), - onFailure: IterableAPIInternal.defaultOnFailure("trackPushOpen")) + onSuccess: nil, + onFailure: nil) } func trackPushOpen(_ campaignId: NSNumber, @@ -114,8 +116,8 @@ extension PushTrackerProtocol { messageId: messageId, appAlreadyRunning: appAlreadyRunning, dataFields: dataFields, - onSuccess: IterableAPIInternal.defaultOnSuccess("trackPushOpen"), - onFailure: IterableAPIInternal.defaultOnFailure("trackPushOpen")) + onSuccess: nil, + onFailure: nil) } } @@ -127,11 +129,11 @@ extension PushTrackerProtocol { extension UIApplication: ApplicationStateProviderProtocol {} struct IterableAppIntegrationInternal { - private let tracker: PushTrackerProtocol + private weak var tracker: PushTrackerProtocol? private let urlDelegate: IterableURLDelegate? private let customActionDelegate: IterableCustomActionDelegate? private let urlOpener: UrlOpenerProtocol? - private let inAppNotifiable: InAppNotifiable + private weak var inAppNotifiable: InAppNotifiable? init(tracker: PushTrackerProtocol, urlDelegate: IterableURLDelegate? = nil, @@ -160,10 +162,10 @@ struct IterableAppIntegrationInternal { if case let NotificationInfo.silentPush(silentPush) = NotificationHelper.inspect(notification: userInfo) { switch silentPush.notificationType { case .update: - _ = inAppNotifiable.scheduleSync() + _ = inAppNotifiable?.scheduleSync() case .remove: if let messageId = silentPush.messageId { - inAppNotifiable.onInAppRemoved(messageId: messageId) + inAppNotifiable?.onInAppRemoved(messageId: messageId) } else { ITBError("messageId not found in 'remove' silent push") } @@ -220,7 +222,7 @@ struct IterableAppIntegrationInternal { // Track push open if let _ = dataFields[JsonKey.actionIdentifier.jsonKey] { // i.e., if action is not dismiss - tracker.trackPushOpen(userInfo, dataFields: dataFields) + tracker?.trackPushOpen(userInfo, dataFields: dataFields) } // Execute the action @@ -312,7 +314,7 @@ struct IterableAppIntegrationInternal { // Track push open let dataFields = [JsonKey.actionIdentifier.jsonKey: JsonValue.ActionIdentifier.pushOpenDefault] - tracker.trackPushOpen(userInfo, dataFields: dataFields) + tracker?.trackPushOpen(userInfo, dataFields: dataFields) guard let itbl = IterableAppIntegrationInternal.itblValue(fromUserInfo: userInfo) else { return @@ -331,7 +333,7 @@ struct IterableAppIntegrationInternal { } private func alreadyTracked(userInfo: [AnyHashable: Any]) -> Bool { - guard let lastPushPayload = tracker.lastPushPayload else { + guard let lastPushPayload = tracker?.lastPushPayload else { return false } diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift new file mode 100644 index 000000000..010c0be05 --- /dev/null +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -0,0 +1,197 @@ +// +// Created by Tapash Majumder on 7/20/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import CoreData +import Foundation + +enum PersistenceConst { + static let dataModelFileName = "IterableDataModel" + static let dataModelExtension = "momd" + + enum Entity { + enum Task { + static let name = "IterableTaskManagedObject" + + enum Column { + static let id = "id" + static let scheduledAt = "scheduledAt" + } + } + } +} + +/// `Bundle.current` is used to find url path for core data model file. +/// This is a temporary fix until we can use `Bundle.module` in IterableSDK. +import class Foundation.Bundle +private class BundleFinder {} +extension Foundation.Bundle { + /// Returns the resource bundle associated with the current Swift module. + static var current: Bundle = { + // This is your `target.path` (located in your `Package.swift`) by replacing all the `/` by the `_`. + let bundleName = "IterableSDK_IterableSDK" + let candidates = [ + // Bundle should be present here when the package is linked into an App. + Bundle.main.resourceURL, + // Bundle should be present here when the package is linked into a framework. + Bundle(for: BundleFinder.self).resourceURL, + // For command-line tools. + Bundle.main.bundleURL, + ] + for candidate in candidates { + let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") + if let bundle = bundlePath.flatMap(Bundle.init(url:)) { + return bundle + } + } + + return Bundle(for: BundleFinder.self) + }() +} + +@available(iOS 10.0, *) +class PersistentContainer: NSPersistentContainer { + static let shared: PersistentContainer? = { + guard let url = Bundle.current.url(forResource: PersistenceConst.dataModelFileName, withExtension: PersistenceConst.dataModelExtension) else { + ITBError("Could not find \(PersistenceConst.dataModelFileName) in bundle") + return nil + } + guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else { + ITBError("Could not initialize managed object model") + return nil + } + + let container = PersistentContainer(name: PersistenceConst.dataModelFileName, managedObjectModel: managedObjectModel) + container.loadPersistentStores { desc, error in + if let error = error { + fatalError("Unresolved error \(error)") + } + + ITBInfo("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")") + } + + container.viewContext.automaticallyMergesChangesFromParent = true + container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType) + + return container + }() + + override func newBackgroundContext() -> NSManagedObjectContext { + let backgroundContext = super.newBackgroundContext() + backgroundContext.automaticallyMergesChangesFromParent = true + backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType) + return backgroundContext + } +} + +@available(iOS 10.0, *) +struct CoreDataPersistenceContextProvider: IterablePersistenceContextProvider { + init?(dateProvider: DateProviderProtocol = SystemDateProvider()) { + guard let persistentContainer = PersistentContainer.shared else { + return nil + } + self.persistentContainer = persistentContainer + self.dateProvider = dateProvider + } + + func newBackgroundContext() -> IterablePersistenceContext { + return CoreDataPersistenceContext(managedObjectContext: persistentContainer.newBackgroundContext(), dateProvider: dateProvider) + } + + func mainQueueContext() -> IterablePersistenceContext { + return CoreDataPersistenceContext(managedObjectContext: persistentContainer.viewContext, dateProvider: dateProvider) + } + + private let persistentContainer: PersistentContainer + private let dateProvider: DateProviderProtocol +} + +@available(iOS 10.0, *) +struct CoreDataPersistenceContext: IterablePersistenceContext { + init(managedObjectContext: NSManagedObjectContext, dateProvider: DateProviderProtocol) { + self.managedObjectContext = managedObjectContext + self.dateProvider = dateProvider + } + + func create(task: IterableTask) throws -> IterableTask { + guard let taskManagedObject = createTaskManagedObject() else { + throw IterableDBError.general("Could not create task managed object") + } + + PersistenceHelper.copy(from: task, to: taskManagedObject) + taskManagedObject.createdAt = dateProvider.currentDate + return PersistenceHelper.task(from: taskManagedObject) + } + + func update(task: IterableTask) throws -> IterableTask { + guard let taskManagedObject = try findTaskManagedObject(id: task.id) else { + throw IterableDBError.general("Could not find task to update") + } + + PersistenceHelper.copy(from: task, to: taskManagedObject) + taskManagedObject.modifiedAt = dateProvider.currentDate + return PersistenceHelper.task(from: taskManagedObject) + } + + func delete(task: IterableTask) throws { + try deleteTask(withId: task.id) + } + + func nextTask() throws -> IterableTask? { + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(context: managedObjectContext, + entity: PersistenceConst.Entity.Task.name, + column: PersistenceConst.Entity.Task.Column.scheduledAt, + ascending: true, + limit: 1) + return taskManagedObjects.first.map(PersistenceHelper.task(from:)) + } + + func findTask(withId id: String) throws -> IterableTask? { + guard let taskManagedObject = try findTaskManagedObject(id: id) else { + return nil + } + return PersistenceHelper.task(from: taskManagedObject) + } + + func deleteTask(withId id: String) throws { + guard let taskManagedObject = try findTaskManagedObject(id: id) else { + return + } + managedObjectContext.delete(taskManagedObject) + } + + func findAllTasks() throws -> [IterableTask] { + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) + + return taskManagedObjects.map(PersistenceHelper.task(from:)) + } + + func deleteAllTasks() throws { + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) + taskManagedObjects.forEach { managedObjectContext.delete($0) } + } + + func save() throws { + try managedObjectContext.save() + } + + func perform(_ block: @escaping () -> Void) { + managedObjectContext.perform(block) + } + + func performAndWait(_ block: () -> Void) { + managedObjectContext.performAndWait(block) + } + + private let managedObjectContext: NSManagedObjectContext + private let dateProvider: DateProviderProtocol + + private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? { + try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name, columnName: PersistenceConst.Entity.Task.Column.id, columnValue: id) + } + + private func createTaskManagedObject() -> IterableTaskManagedObject? { + CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) + } +} diff --git a/swift-sdk/Internal/IterableNotifications.swift b/swift-sdk/Internal/IterableNotifications.swift new file mode 100644 index 000000000..e3a9b3744 --- /dev/null +++ b/swift-sdk/Internal/IterableNotifications.swift @@ -0,0 +1,84 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +protocol NotificationCenterProtocol { + func addObserver(_ observer: Any, selector: Selector, name: Notification.Name?, object: Any?) + func removeObserver(_ observer: Any) + func post(name: Notification.Name, object: Any?, userInfo: [AnyHashable: Any]?) +} + +extension NotificationCenter: NotificationCenterProtocol {} + +extension Notification.Name { + static let iterableTaskScheduled = Notification.Name(rawValue: "itbl_task_scheduled") + static let iterableTaskFinishedWithSuccess = Notification.Name(rawValue: "itbl_task_finished_with_success") + static let iterableTaskFinishedWithRetry = Notification.Name(rawValue: "itbl_task_finished_with_retry") + static let iterableTaskFinishedWithNoRetry = Notification.Name(rawValue: "itbl_task_finished_with_no_retry") + static let iterableNetworkOffline = Notification.Name(rawValue: "itbl_network_offline") + static let iterableNetworkOnline = Notification.Name(rawValue: "itbl_network_online") +} + +struct TaskSendRequestValue { + let taskId: String + let sendRequestValue: SendRequestValue +} + +struct TaskSendRequestError { + let taskId: String + let sendRequestError: SendRequestError +} + +struct IterableNotificationUtil { + static func sendRequestValueToUserInfo(_ sendRequestValue: SendRequestValue, taskId: String) -> [AnyHashable: Any] { + var userInfo = [AnyHashable: Any]() + userInfo[Key.taskId] = taskId + userInfo[Key.sendRequestValue] = sendRequestValue + return userInfo + } + + static func sendRequestErrorToUserInfo(_ sendRequestError: SendRequestError, taskId: String) -> [AnyHashable: Any] { + var userInfo = [AnyHashable: Any]() + userInfo[Key.taskId] = taskId + userInfo[Key.sendRequestError] = sendRequestError + return userInfo + } + + static func notificationToTaskSendRequestValue(_ notification: Notification) -> TaskSendRequestValue? { + guard let userInfo = notification.userInfo else { + return nil + } + guard let taskId = userInfo[Key.taskId] as? String else { + return nil + } + guard let sendRequestValue = userInfo[Key.sendRequestValue] as? SendRequestValue else { + return nil + } + + return TaskSendRequestValue(taskId: taskId, sendRequestValue: sendRequestValue) + } + + static func notificationToTaskSendRequestError(_ notification: Notification) -> TaskSendRequestError? { + guard let userInfo = notification.userInfo else { + return nil + } + guard let taskId = userInfo[Key.taskId] as? String else { + return nil + } + guard let sendRequestError = userInfo[Key.sendRequestError] as? SendRequestError else { + return nil + } + + return TaskSendRequestError(taskId: taskId, sendRequestError: sendRequestError) + } + + private enum Key { + static let taskId = "taskId" + static let sendRequestValue = "sendRequestValue" + static let sendRequestError = "sendRequestError" + } + +} diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift new file mode 100644 index 000000000..df46d3f44 --- /dev/null +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -0,0 +1,52 @@ +// +// Created by Tapash Majumder on 7/20/20. +// Copyright © 2020 Iterable. All rights reserved. +// +// This defines persistence contracts for Iterable. +// This should not be dependent on Coredata + +import Foundation + +enum IterableDBError: Error { + case general(String) +} + +extension IterableDBError: LocalizedError { + var errorDescription: String? { + switch self { + case let .general(description): + return description + } + } +} + +protocol IterablePersistenceContext { + @discardableResult + func create(task: IterableTask) throws -> IterableTask + + @discardableResult + func update(task: IterableTask) throws -> IterableTask + + func delete(task: IterableTask) throws + + func findTask(withId id: String) throws -> IterableTask? + + func deleteTask(withId id: String) throws + + func nextTask() throws -> IterableTask? + + func findAllTasks() throws -> [IterableTask] + + func deleteAllTasks() throws + + func save() throws + + func perform(_ block: @escaping () -> Void) + + func performAndWait(_ block: () -> Void) +} + +protocol IterablePersistenceContextProvider { + func newBackgroundContext() -> IterablePersistenceContext + func mainQueueContext() -> IterablePersistenceContext +} diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift new file mode 100644 index 000000000..b2de3c2a5 --- /dev/null +++ b/swift-sdk/Internal/IterableRequest.swift @@ -0,0 +1,95 @@ +// +// Created by Tapash Majumder on 7/29/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +// These are Iterable specific Request items. +// They don't have Api endpoint and request endpoint defined yet. +enum IterableRequest { + case get(GetRequest) + case post(PostRequest) +} + +extension IterableRequest: Codable { + enum CodingKeys: String, CodingKey { + case type + case value + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case IterableRequest.requestTypeGet: + let request = try container.decode(GetRequest.self, forKey: .value) + self = .get(request) + case IterableRequest.requestTypePost: + let request = try container.decode(PostRequest.self, forKey: .value) + self = .post(request) + default: + throw IterableError.general(description: "Unknown request type: \(type)") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .get(request): + try container.encode(IterableRequest.requestTypeGet, forKey: .type) + try container.encode(request, forKey: .value) + case let .post(request): + try container.encode(IterableRequest.requestTypePost, forKey: .type) + try container.encode(request, forKey: .value) + } + } + + private static let requestTypeGet = "get" + private static let requestTypePost = "post" +} + +struct GetRequest: Codable { + let path: String + let args: [String: String]? +} + +struct PostRequest { + let path: String + let args: [String: String]? + let body: [AnyHashable: Any]? +} + +extension PostRequest: Codable { + enum CodingKeys: String, CodingKey { + case path + case args + case body + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let path = try container.decode(String.self, forKey: .path) + let args = try container.decode([String: String]?.self, forKey: .args) + let body: [AnyHashable: Any]? + if let bodyData = try container.decode(Data?.self, forKey: .body) { + body = try JSONSerialization.jsonObject(with: bodyData, options: []) as? [AnyHashable: Any] + } else { + body = nil + } + self.path = path + self.args = args + self.body = body + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(path, forKey: .path) + try container.encode(args, forKey: .args) + var bodyData: Data? + if let body = self.body, JSONSerialization.isValidJSONObject(body) { + bodyData = try JSONSerialization.data(withJSONObject: body, options: []) + } + try container.encode(bodyData, forKey: .body) + } +} diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift new file mode 100644 index 000000000..1c15cd192 --- /dev/null +++ b/swift-sdk/Internal/IterableTask.swift @@ -0,0 +1,82 @@ +// +// Created by Tapash Majumder on 7/20/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct IterableTask { + static let currentVersion = 1 + + let id: String + let name: String? + let version: Int + let createdAt: Date? + let modifiedAt: Date? + let type: IterableTaskType + let attempts: Int + let lastAttemptedAt: Date? + let processing: Bool + let scheduledAt: Date + let data: Data? + let failed: Bool + let blocking: Bool + let requestedAt: Date + let taskFailureData: Data? + + init(id: String, + name: String? = nil, + version: Int = IterableTask.currentVersion, + createdAt: Date? = nil, + modifiedAt: Date? = nil, + type: IterableTaskType, + attempts: Int = 0, + lastAttemptedAt: Date? = nil, + processing: Bool = false, + scheduledAt: Date, + data: Data? = nil, + failed: Bool = false, + blocking: Bool = true, + requestedAt: Date, + taskFailureData: Data? = nil) { + self.id = id + self.name = name + self.version = version + self.createdAt = createdAt + self.modifiedAt = modifiedAt + self.type = type + self.attempts = attempts + self.lastAttemptedAt = lastAttemptedAt + self.processing = processing + self.scheduledAt = scheduledAt + self.data = data + self.failed = failed + self.blocking = blocking + self.requestedAt = requestedAt + self.taskFailureData = taskFailureData + } + + func updated(attempts: Int? = nil, + lastAttemptedAt: Date? = nil, + processing: Bool? = nil, + scheduledAt: Date? = nil, + data: Data? = nil, + failed: Bool? = nil, + taskFailureData: Data? = nil) -> IterableTask { + IterableTask(id: id, + name: name, + version: version, + createdAt: createdAt, + modifiedAt: modifiedAt, + type: type, + attempts: attempts ?? self.attempts, + lastAttemptedAt: lastAttemptedAt ?? self.lastAttemptedAt, + processing: processing ?? self.processing, + scheduledAt: scheduledAt ?? self.scheduledAt, + data: data ?? self.data, + failed: failed ?? false, + blocking: blocking, + requestedAt: requestedAt, + taskFailureData: taskFailureData ?? self.taskFailureData) + } +} diff --git a/swift-sdk/Internal/IterableTaskError.swift b/swift-sdk/Internal/IterableTaskError.swift new file mode 100644 index 000000000..9c73bf2fc --- /dev/null +++ b/swift-sdk/Internal/IterableTaskError.swift @@ -0,0 +1,23 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +enum IterableTaskError: Error { + case general(String?) + + static func createErroredFuture(reason: String? = nil) -> Future { + Promise(error: IterableTaskError.general(reason)) + } +} + +extension IterableTaskError: LocalizedError { + var errorDescription: String? { + switch self { + case let .general(description): + return description ?? "general error" + } + } +} diff --git a/swift-sdk/Internal/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift new file mode 100644 index 000000000..86eb0dd55 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -0,0 +1,34 @@ +// +// Created by Tapash Majumder on 7/22/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +/// This class mirrors what is automatically generated by Xcode. + +import CoreData +import Foundation + +@objc(IterableTaskManagedObject) +public class IterableTaskManagedObject: NSManagedObject {} + +extension IterableTaskManagedObject { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: PersistenceConst.Entity.Task.name) + } + + @NSManaged public var attempts: Int64 + @NSManaged public var createdAt: Date? + @NSManaged public var modifiedAt: Date? + @NSManaged public var data: Data? + @NSManaged public var id: String + @NSManaged public var name: String? + @NSManaged public var lastAttemptedAt: Date? + @NSManaged public var processing: Bool + @NSManaged public var type: String + @NSManaged public var scheduledAt: Date + @NSManaged public var failed: Bool + @NSManaged public var blocking: Bool + @NSManaged public var requestedAt: Date + @NSManaged public var taskFailureData: Data? + @NSManaged public var version: Int64 +} diff --git a/swift-sdk/Internal/IterableTaskProcessor.swift b/swift-sdk/Internal/IterableTaskProcessor.swift new file mode 100644 index 000000000..88b6ced2c --- /dev/null +++ b/swift-sdk/Internal/IterableTaskProcessor.swift @@ -0,0 +1,18 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +enum IterableTaskType: String { + case apiCall +} + +struct IterableTaskContext { + let blocking: Bool +} + +protocol IterableTaskProcessor { + func process(task: IterableTask) throws -> Future +} diff --git a/swift-sdk/Internal/IterableTaskResult.swift b/swift-sdk/Internal/IterableTaskResult.swift new file mode 100644 index 000000000..a5b3dcea3 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskResult.swift @@ -0,0 +1,20 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +enum IterableTaskResult { + case success(detail: TaskSuccessDetail?) + case failureWithRetry(retryAfter: TimeInterval?, detail: TaskFailureDetail?) + case failureWithNoRetry(detail: TaskFailureDetail?) +} + +protocol TaskSuccessDetail {} + +extension SendRequestValue: TaskSuccessDetail {} + +protocol TaskFailureDetail {} + +extension SendRequestError: TaskFailureDetail {} diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift new file mode 100644 index 000000000..85901b087 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -0,0 +1,264 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation +import UIKit + + +@available(iOS 10.0, *) +class IterableTaskRunner: NSObject { + init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), + persistenceContextProvider: IterablePersistenceContextProvider, + notificationCenter: NotificationCenterProtocol = NotificationCenter.default, + timeInterval: TimeInterval = 1.0 * 60, + connectivityManager: NetworkConnectivityManager = NetworkConnectivityManager()) { + ITBInfo() + self.networkSession = networkSession + self.persistenceContextProvider = persistenceContextProvider + self.notificationCenter = notificationCenter + self.timeInterval = timeInterval + self.connectivityManager = connectivityManager + + super.init() + + self.notificationCenter.addObserver(self, + selector: #selector(onTaskScheduled(notification:)), + name: .iterableTaskScheduled, + object: nil) + self.notificationCenter.addObserver(self, + selector: #selector(onAppWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil) + self.notificationCenter.addObserver(self, + selector: #selector(onAppDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + self.connectivityManager.connectivityChangedCallback = { [weak self] in self?.onConnectivityChanged(connected: $0) } + } + + func start() { + ITBInfo() + paused = false + run() + connectivityManager.start() + } + + func stop() { + ITBInfo() + paused = true + timer?.invalidate() + timer = nil + connectivityManager.stop() + } + + @objc + private func onTaskScheduled(notification: Notification) { + ITBInfo() + if !running && !paused { + runNow() + } + } + + @objc + private func onAppWillEnterForeground(notification _: Notification) { + ITBInfo() + start() + } + + @objc + private func onAppDidEnterBackground(notification _: Notification) { + ITBInfo() + stop() + } + + private func runNow() { + timer?.invalidate() + timer = nil + run() + } + + private func onConnectivityChanged(connected: Bool) { + ITBInfo() + if connected { + if paused { + paused = false + if !running { + runNow() + } + } + } else { + if !paused { + paused = true + } + } + } + + private func run() { + ITBInfo() + guard !paused else { + ITBInfo("Cannot run when paused") + return + } + guard !running else { + ITBInfo("Already running") + return + } + + persistenceContext.perform { + self.processTasks().onSuccess { _ in + ITBInfo("Done processing tasks") + self.running = false + self.scheduleNext() + } + } + } + + private func scheduleNext() { + ITBInfo() + guard !paused else { + ITBInfo("Paused") + return + } + + DispatchQueue.global().async { [weak self] in + ITBInfo("Scheduling timer") + guard let timeInterval = self?.timeInterval else { + return + } + + let timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in + self?.run() + } + self?.timer = timer + RunLoop.current.add(timer, forMode: .default) + RunLoop.current.run() + } + } + + @discardableResult + private func processTasks() -> Future { + ITBInfo() + running = true + + /// This is a recursive function. + /// Check whether we were stopped in the middle of running tasks + guard !paused else { + ITBInfo("Tasks paused before finishing processTasks()") + return Promise(value: ()) + } + + if let task = try? persistenceContext.nextTask() { + return execute(task: task).flatMap { executionResult in + switch executionResult { + case .success, .failure, .error: + self.deleteTask(task: task) + return self.processTasks() + case .processing, .retry: + return Promise(value: ()) + } + } + } else { + ITBInfo("No tasks to execute") + return Promise(value: ()) + } + } + + @discardableResult + private func execute(task: IterableTask) -> Future { + ITBInfo("Executing taskId: \(task.id), name: \(task.name ?? "nil")") + guard task.processing == false else { + return Promise(value: .processing) + } + + switch task.type { + case .apiCall: + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + return processAPICallTask(processor: processor, task: task) + } + } + + private func processAPICallTask(processor: IterableAPICallTaskProcessor, + task: IterableTask) -> Future { + ITBInfo() + let result = Promise() + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + do { + try processor.process(task: task).onSuccess { taskResult in + switch taskResult { + case let .success(detail: detail): + ITBInfo("task: \(task.id) succeeded") + if let successDetail = detail as? SendRequestValue { + let userInfo = IterableNotificationUtil.sendRequestValueToUserInfo(successDetail, taskId: task.id) + self.notificationCenter.post(name: .iterableTaskFinishedWithSuccess, + object: self, + userInfo: userInfo) + } + result.resolve(with: .success) + case let .failureWithNoRetry(detail: detail): + ITBInfo("task: \(task.id) failed with no retry.") + if let failureDetail = detail as? SendRequestError { + let userInfo = IterableNotificationUtil.sendRequestErrorToUserInfo(failureDetail, taskId: task.id) + self.notificationCenter.post(name: .iterableTaskFinishedWithNoRetry, + object: self, + userInfo: userInfo) + } + result.resolve(with: .failure) + case let .failureWithRetry(_, detail: detail): + ITBInfo("task: \(task.id) processed with retry") + if let failureDetail = detail as? SendRequestError { + let userInfo = IterableNotificationUtil.sendRequestErrorToUserInfo(failureDetail, taskId: task.id) + self.notificationCenter.post(name: .iterableTaskFinishedWithRetry, + object: self, + userInfo: userInfo) + } + result.resolve(with: .retry) + } + }.onError { error in + ITBError("task processing error: \(error.localizedDescription)") + result.resolve(with: .failure) + } + } catch let error { + ITBError("Error proessing task: \(task.id), message: \(error.localizedDescription)") + result.resolve(with: .error) + } + return result + } + + deinit { + ITBInfo() + stop() + notificationCenter.removeObserver(self) + } + + private func deleteTask(task: IterableTask) { + do { + try persistenceContext.delete(task: task) + try persistenceContext.save() + } catch let error { + ITBError(error.localizedDescription) + } + } + + private enum TaskExecutionResult { + case processing + case success + case failure + case retry + case error + } + + private var paused = false + private let networkSession: NetworkSessionProtocol + private let persistenceContextProvider: IterablePersistenceContextProvider + private let notificationCenter: NotificationCenterProtocol + private let timeInterval: TimeInterval + private let connectivityManager: NetworkConnectivityManager + private weak var timer: Timer? + private var running = false + + private lazy var persistenceContext: IterablePersistenceContext = { + return persistenceContextProvider.newBackgroundContext() + }() +} diff --git a/swift-sdk/Internal/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift new file mode 100644 index 000000000..74667a5e2 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -0,0 +1,48 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +@available(iOS 10.0, *) +class IterableTaskScheduler { + init(persistenceContextProvider: IterablePersistenceContextProvider, + notificationCenter: NotificationCenterProtocol = NotificationCenter.default, + dateProvider: DateProviderProtocol = SystemDateProvider()) { + self.persistenceContextProvider = persistenceContextProvider + self.notificationCenter = notificationCenter + self.dateProvider = dateProvider + } + + func schedule(apiCallRequest: IterableAPICallRequest, + context: IterableTaskContext = IterableTaskContext(blocking: true), + scheduledAt: Date? = nil) -> Result { + ITBInfo() + let taskId = IterableUtil.generateUUID() + do { + let data = try JSONEncoder().encode(apiCallRequest) + + try persistenceContext.create(task: IterableTask(id: taskId, + name: apiCallRequest.getPath(), + type: .apiCall, + scheduledAt: scheduledAt ?? dateProvider.currentDate, + data: data, + requestedAt: dateProvider.currentDate)) + try persistenceContext.save() + + notificationCenter.post(name: .iterableTaskScheduled, object: self, userInfo: nil) + } catch let error { + return Result.failure(IterableTaskError.general("schedule taskId: \(taskId) failed with error: \(error.localizedDescription)")) + } + return Result.success(taskId) + } + + private let persistenceContextProvider: IterablePersistenceContextProvider + private let notificationCenter: NotificationCenterProtocol + private let dateProvider: DateProviderProtocol + + private lazy var persistenceContext: IterablePersistenceContext = { + return persistenceContextProvider.newBackgroundContext() + }() +} diff --git a/swift-sdk/Internal/NetworkConnectivityChecker.swift b/swift-sdk/Internal/NetworkConnectivityChecker.swift new file mode 100644 index 000000000..f3ac21059 --- /dev/null +++ b/swift-sdk/Internal/NetworkConnectivityChecker.swift @@ -0,0 +1,113 @@ +// +// Created by Tapash Majumder on 9/2/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct NetworkConnectivityChecker { + /// first argument is successfulChecks, + /// second argument is totalChecks + /// Should return true if threshold is met + typealias IsThresholdMet = (Int, Int) -> Bool + + init(networkSession: NetworkSessionProtocol? = nil, + isThresholdMet: IsThresholdMet? = nil) { + self.networkSession = networkSession ?? URLSession(configuration: Self.urlSessionConfiguration) + self.isThresholdMet = isThresholdMet ?? Self.defaultIsThresholdMet + } + + @discardableResult + func checkConnectivity() -> Future { + let result = Promise() + let dispatchGroup = DispatchGroup() + var tasks: [DataTaskProtocol] = [] + var successfulChecks: Int = 0, failedChecks: Int = 0 + let totalChecks = Self.urlsToCheck.count + + let completionHandlerForUrl: (URL) -> NetworkSessionProtocol.CompletionHandler = { url in + return { data, response, error in + let success = self.checkSucceeded(for: url, data: data, response: response, error: error) + success ? (successfulChecks += 1) : (failedChecks += 1) + dispatchGroup.leave() + + // If enough tasks have finished successfully abort early + self.cancelCheck( + pendingTasks: tasks, + successfulChecks: successfulChecks, + totalChecks: totalChecks + ) + } + } + + tasks = Self.urlsToCheck.map { + return networkSession.createDataTask(with: $0, completionHandler: completionHandlerForUrl($0)) + } + + tasks.forEach { task in + dispatchGroup.enter() + tasksQueue.async { + task.resume() + } + } + + dispatchGroup.notify(queue: updateQueue) { [self] in + let online = self.isThresholdMet(successfulChecks, totalChecks) + result.resolve(with: online) + } + + return result + } + + private func checkSucceeded(for url: URL, data: Data?, response: URLResponse?, error: Error?) -> Bool { + if let error = error { + ITBError("error checking status, error: \(error.localizedDescription)") + return false + } + guard let response = response as? HTTPURLResponse else { + ITBError("No response") + return false + } + + return (200..<300).contains(response.statusCode) + } + + private func cancelCheck( + pendingTasks: [DataTaskProtocol], + successfulChecks: Int, + totalChecks: Int) { + let connected = isThresholdMet(successfulChecks, totalChecks) + guard connected else { return } + + cancelPendingTasks(pendingTasks) + } + + private func cancelPendingTasks(_ tasks: [DataTaskProtocol]) { + for task in tasks where [.running, .suspended].contains(task.state) { + task.cancel() + } + } + + private static let urlSessionConfiguration: URLSessionConfiguration = { + let sessionConfiguration = URLSessionConfiguration.default + sessionConfiguration.requestCachePolicy = .reloadIgnoringCacheData + sessionConfiguration.timeoutIntervalForRequest = 5.0 + sessionConfiguration.timeoutIntervalForResource = 5.0 + return sessionConfiguration + }() + + private static let urlsToCheck: [URL] = [ + "https://www.apple.com/library/test/success.html", + "https://www.google.com" + ].compactMap { URL(string: $0) } + + private static let defaultIsThresholdMet: (Int, Int) -> Bool = { value, outOf in + (Double(value) / Double(outOf)) * 100.0 >= 50.0 + } + + private var networkSession: NetworkSessionProtocol + private var isThresholdMet: IsThresholdMet + + private let tasksQueue = DispatchQueue(label: "tasksQueue") + private var updateQueue = DispatchQueue(label: "updateQueue") +} diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift new file mode 100644 index 000000000..3443a55fc --- /dev/null +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -0,0 +1,126 @@ +// +// Created by Tapash Majumder on 9/8/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +class NetworkConnectivityManager: NSObject { + init(networkMonitor: NetworkMonitorProtocol? = nil, + connectivityChecker: NetworkConnectivityChecker = NetworkConnectivityChecker(), + notificationCenter: NotificationCenterProtocol = NotificationCenter.default, + offlineModePollingInterval: TimeInterval? = nil, + onlineModePollingInterval: TimeInterval? = nil) { + ITBInfo() + self.networkMonitor = networkMonitor ?? Self.createNetworkMonitor() + self.connectivityChecker = connectivityChecker + self.notificationCenter = notificationCenter + self.offlineModePollingInterval = offlineModePollingInterval ?? Self.defaultOfflineModePollingInterval + self.onlineModePollingInterval = onlineModePollingInterval ?? Self.defaultOnlineModePollingInterval + super.init() + notificationCenter.addObserver(self, + selector: #selector(onNetworkOnline(notification:)), + name: .iterableNetworkOnline, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(onNetworkOffline(notification:)), + name: .iterableNetworkOffline, + object: nil) + } + + deinit { + ITBInfo() + notificationCenter.removeObserver(self) + stop() + } + + var isOnline: Bool { + online + } + + var connectivityChangedCallback: ((Bool) -> Void)? + + func start() { + ITBInfo() + networkMonitor.statusUpdatedCallback = { [weak self] in self?.updateStatus() } + networkMonitor.start() + startTimer() + } + + func stop() { + ITBInfo() + networkMonitor.stop() + stopTimer() + } + + private func startTimer() { + ITBInfo() + let interval = online ? onlineModePollingInterval : offlineModePollingInterval + if #available(iOS 10.0, *) { + timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in + ITBInfo("timer called") + self?.updateStatus() + }) + } + } + + private func stopTimer() { + ITBInfo() + timer?.invalidate() + timer = nil + } + + private func resetTimer() { + ITBInfo() + stopTimer() + startTimer() + } + + private func updateStatus() { + ITBInfo() + connectivityChecker.checkConnectivity().onSuccess { connected in + self.online = connected + } + } + + @objc + private func onNetworkOnline(notification _: Notification) { + ITBInfo() + online = true + } + + @objc + private func onNetworkOffline(notification _: Notification) { + ITBInfo() + online = false + } + + private static func createNetworkMonitor() -> NetworkMonitorProtocol { + if #available(iOS 12, *) { + return NetworkMonitor() + } else { + return PollingNetworkMonitor() + } + } + + private let notificationCenter: NotificationCenterProtocol + private var networkMonitor: NetworkMonitorProtocol + private let connectivityChecker: NetworkConnectivityChecker + private var timer: Timer? + private let offlineModePollingInterval: TimeInterval + private let onlineModePollingInterval: TimeInterval + private static let defaultOfflineModePollingInterval: TimeInterval = 1 * 60.0 + private static let defaultOnlineModePollingInterval: TimeInterval = 10 * 60 + + private var online = true { + didSet { + ITBInfo("online: \(online)") + if online != oldValue { + ITBInfo("connectivity changed") + connectivityChangedCallback?(online) + resetTimer() + } + } + } +} + diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 2751531a0..c69bacaee 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -33,18 +33,60 @@ struct SendRequestError: Error { static func from(error: Error) -> SendRequestError { SendRequestError(reason: error.localizedDescription) } + + static func from(networkError: NetworkError, + reason: String? = nil, + iterableCode: String? = nil) -> SendRequestError { + SendRequestError(reason: reason ?? networkError.reason, + data: networkError.data, + httpStatusCode: networkError.httpStatusCode, + iterableCode: iterableCode, + originalError: networkError.originalError) + } } extension SendRequestError: LocalizedError { - var localizedDescription: String { - reason ?? "" + var errorDescription: String? { + reason + } +} + +struct NetworkError: Error { + let reason: String? + let data: Data? + let httpStatusCode: Int? + let originalError: Error? + + init(reason: String? = nil, + data: Data? = nil, + httpStatusCode: Int? = nil, + originalError: Error? = nil) { + self.reason = reason + self.data = data + self.httpStatusCode = httpStatusCode + self.originalError = originalError + } +} + +extension NetworkError: LocalizedError { + var errorDescription: String? { + reason } } +protocol DataTaskProtocol { + var state: URLSessionDataTask.State { get } + func resume() + func cancel() +} + +extension URLSessionDataTask: DataTaskProtocol {} + protocol NetworkSessionProtocol { typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void func makeRequest(_ request: URLRequest, completionHandler: @escaping CompletionHandler) func makeDataRequest(with url: URL, completionHandler: @escaping CompletionHandler) + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol } extension URLSession: NetworkSessionProtocol { @@ -63,6 +105,10 @@ extension URLSession: NetworkSessionProtocol { task.resume() } + + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { + dataTask(with: url, completionHandler: completionHandler) + } } struct NetworkHelper { @@ -87,8 +133,9 @@ struct NetworkHelper { return promise } - static func sendRequest(_ request: URLRequest, - usingSession networkSession: NetworkSessionProtocol) -> Future { + static func sendRequest(_ request: URLRequest, + converter: @escaping (Data) throws -> T?, + usingSession networkSession: NetworkSessionProtocol) -> Future { #if NETWORK_DEBUG let requestId = IterableUtil.generateUUID() print() @@ -109,10 +156,13 @@ struct NetworkHelper { print() #endif - let promise = Promise() + let promise = Promise() networkSession.makeRequest(request) { data, response, error in - let result = createResultFromNetworkResponse(data: data, response: response, error: error) + let result = createResultFromNetworkResponse(data: data, + converter: converter, + response: response, + error: error) switch result { case let .success(value): @@ -131,83 +181,111 @@ struct NetworkHelper { return promise } - static func createResultFromNetworkResponse(data: Data?, - response: URLResponse?, - error: Error?) -> Result { + static func sendRequest(_ request: URLRequest, + usingSession networkSession: NetworkSessionProtocol) -> Future { + let converter: (Data) throws -> SendRequestValue? = { data in + try JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any] + } + + return sendRequest(request, + converter: converter, + usingSession: networkSession) + .mapFailure(convertNetworkErrorToSendRequestError(_:)) + } + + private static func createResultFromNetworkResponse(data: Data?, + converter: (Data) throws -> T?, + response: URLResponse?, + error: Error?) -> Result { if let error = error { - return .failure(SendRequestError(reason: "\(error.localizedDescription)", data: data, originalError: error)) + return .failure(NetworkError(reason: "\(error.localizedDescription)", data: data, originalError: error)) } guard let response = response as? HTTPURLResponse else { - return .failure(SendRequestError(reason: "No response", data: nil)) + return .failure(NetworkError(reason: "No response", data: nil)) } - let responseCode = response.statusCode + let httpStatusCode = response.statusCode - let json: Any? - var jsonError: Error? + if httpStatusCode >= 500 { + return .failure(NetworkError(reason: "Internal Server Error", data: data, httpStatusCode: httpStatusCode)) + } else if httpStatusCode >= 400 { + return .failure(NetworkError(reason: "Invalid Request", data: data, httpStatusCode: httpStatusCode)) + } else if httpStatusCode == 200 { + if let data = data, data.count > 0 { + return convertData(data: data, converter: converter) + } else { + return .failure(NetworkError(reason: "No data received", data: data, httpStatusCode: httpStatusCode)) + } + } else { + return .failure(NetworkError(reason: "Received non-200 response: \(httpStatusCode)", data: data, httpStatusCode: httpStatusCode)) + } + } + + private static func convertData(data: Data, converter: (Data) throws -> T?) -> Result { + do { + if let responseObj = try converter(data) { + return .success(responseObj) + } else { + return .failure(NetworkError(reason: "Wrong response type", data: data, httpStatusCode: 200)) + } + } catch { + var reason = "Could not convert data, error: \(error.localizedDescription)" + if let stringValue = String(data: data, encoding: .utf8) { + reason = "Could not convert data: \(stringValue), error: \(error.localizedDescription)" + } + return .failure(NetworkError(reason: reason, data: data, httpStatusCode: 200)) + } + } + + private static func createDataResultFromNetworkResponse(data: Data?, + response _: URLResponse?, + error: Error?) -> Result { + if let error = error { + return .failure(SendRequestError(reason: "\(error.localizedDescription)")) + } + + guard let data = data else { + return .failure(SendRequestError(reason: "No data")) + } + + return .success(data) + } + + private static func convertNetworkErrorToSendRequestError(_ networkError: NetworkError) -> SendRequestError { + guard let httpStatusCode = networkError.httpStatusCode else { + return SendRequestError.from(networkError: networkError) + } - if let data = data, data.count > 0 { + let json: Any? + if let data = networkError.data, data.count > 0 { do { json = try JSONSerialization.jsonObject(with: data, options: []) } catch { - jsonError = error json = nil } } else { json = nil } - if responseCode == 401 { + if httpStatusCode == 401 { var iterableCode: String? = nil - if let jsonDict = json as? [AnyHashable: Any] { iterableCode = jsonDict[JsonKey.Response.iterableCode] as? String } - return .failure(SendRequestError(reason: "Invalid API Key", data: data, httpStatusCode: responseCode, iterableCode: iterableCode)) - } else if responseCode >= 400 { + return SendRequestError.from(networkError: networkError, reason: "Invalid API Key", iterableCode: iterableCode) + } else if httpStatusCode >= 400 { var reason = "Invalid Request" if let jsonDict = json as? [AnyHashable: Any], let msgFromDict = jsonDict["msg"] as? String { reason = msgFromDict - } else if responseCode >= 500 { + } else if httpStatusCode >= 500 { reason = "Internal Server Error" } - return .failure(SendRequestError(reason: reason, data: data, httpStatusCode: responseCode)) - } else if responseCode == 200 { - if let data = data, data.count > 0 { - if let jsonError = jsonError { - var reason = "Could not parse json, error: \(jsonError.localizedDescription)" - if let stringValue = String(data: data, encoding: .utf8) { - reason = "Could not parse json: \(stringValue), error: \(jsonError.localizedDescription)" - } - - return .failure(SendRequestError(reason: reason, data: data, httpStatusCode: responseCode)) - } else if let json = json as? [AnyHashable: Any] { - return .success(json) - } else { - return .failure(SendRequestError(reason: "Response is not a dictionary", data: data, httpStatusCode: responseCode)) - } - } else { - return .failure(SendRequestError(reason: "No data received", data: data, httpStatusCode: responseCode)) - } - } else { - return .failure(SendRequestError(reason: "Received non-200 response: \(responseCode)", data: data, httpStatusCode: responseCode)) - } - } - - static func createDataResultFromNetworkResponse(data: Data?, - response _: URLResponse?, - error: Error?) -> Result { - if let error = error { - return .failure(SendRequestError(reason: "\(error.localizedDescription)")) + return SendRequestError.from(networkError: networkError, reason: reason) } - guard let data = data else { - return .failure(SendRequestError(reason: "No data")) - } - - return .success(data) + return SendRequestError.from(networkError: networkError) } } diff --git a/swift-sdk/Internal/NetworkMonitor.swift b/swift-sdk/Internal/NetworkMonitor.swift new file mode 100644 index 000000000..e0692deea --- /dev/null +++ b/swift-sdk/Internal/NetworkMonitor.swift @@ -0,0 +1,90 @@ +// +// Created by Tapash Majumder on 9/8/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +#if canImport(Network) +import Network +#endif + +/// Listens to network interface to detect status change. +/// It only knows that the status has changed. +/// It does not know if the network is online or not. +protocol NetworkMonitorProtocol { + func start() + func stop() + var statusUpdatedCallback: (() -> Void)? { get set } +} + +@available(iOS 12.0, *) +class NetworkMonitor: NetworkMonitorProtocol { + init() { + ITBInfo() + } + + deinit { + ITBInfo() + stop() + } + + var statusUpdatedCallback: (() -> Void)? + + func start() { + ITBInfo() + let networkMonitor = NWPathMonitor() + networkMonitor.pathUpdateHandler = { path in + ITBInfo("networkMonitor.pathUpdateHandler, path: \(path.debugDescription), status: \(path.status)") + self.statusUpdatedCallback?() + } + + networkMonitor.start(queue: queue) + self.networkMonitor = networkMonitor + } + + func stop() { + ITBInfo() + networkMonitor?.cancel() + networkMonitor = nil + } + + private weak var networkMonitor: NWPathMonitor? + private let queue = DispatchQueue(label: "NetworkMonitor") +} + +/// This is used for pre-iOS 12.0 because `NWPathMonitor` is not available. +class PollingNetworkMonitor: NetworkMonitorProtocol { + init(pollingInterval: TimeInterval? = nil) { + ITBInfo() + self.pollingInterval = pollingInterval ?? Self.defaultPollingInterval + } + + deinit { + ITBInfo() + } + + var statusUpdatedCallback: (() -> Void)? + + func start() { + ITBInfo() + timer?.invalidate() + if #available(iOS 10.0, *) { + self.timer = Timer.scheduledTimer(withTimeInterval: pollingInterval, repeats: true) { timer in + ITBInfo("Called timer") + self.statusUpdatedCallback?() + } + } + } + + func stop() { + ITBInfo() + timer?.invalidate() + timer = nil + } + + private var pollingInterval: TimeInterval + private static let defaultPollingInterval: TimeInterval = 5 * 60 + + private var timer: Timer? +} diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift new file mode 100644 index 000000000..296faff15 --- /dev/null +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -0,0 +1,447 @@ +// +// Created by Tapash Majumder on 8/24/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +@available(iOS 10.0, *) +struct OfflineRequestProcessor: RequestProcessorProtocol { + init(apiKey: String, + authProvider: AuthProvider?, + authManager: IterableInternalAuthManagerProtocol?, + endPoint: String, + deviceMetadata: DeviceMetadata, + taskScheduler: IterableTaskScheduler, + taskRunner: IterableTaskRunner, + notificationCenter: NotificationCenterProtocol + ) { + ITBInfo() + self.apiKey = apiKey + self.authProvider = authProvider + self.authManager = authManager + self.endPoint = endPoint + self.deviceMetadata = deviceMetadata + self.taskScheduler = taskScheduler + self.taskRunner = taskRunner + notificationListener = NotificationListener(notificationCenter: notificationCenter) + } + + func start() { + ITBInfo() + taskRunner.start() + } + + func stop(){ + ITBInfo() + taskRunner.stop() + } + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, + notificationsEnabled: true) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createDisableDeviceRequest(forAllUsers: false, hexToken: hexToken) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createDisableDeviceRequest(forAllUsers: true, hexToken: hexToken) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createUpdateUserRequest(dataFields: dataFields, mergeNestedObjects: mergeNestedObjects) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func updateEmail(_ newEmail: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createUpdateEmailRequest(newEmail: newEmail) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackPurchaseRequest(total, + items: items, + dataFields: dataFields) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackPushOpenRequest(campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + ITBInfo() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackEventRequest(event, + dataFields: dataFields) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createUpdateSubscriptionsRequest(info.emailListIds, + unsubscribedChannelIds: info.unsubscribedChannelIds, + unsubscribedMessageTypeIds: info.unsubscribedMessageTypeIds, + subscribedMessageTypeIds: info.subscribedMessageTypeIds, + campaignId: info.campaignId, + templateId: info.templateId) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppOpenRequest(inAppMessageContext: InAppMessageContext.from(message: message, + location: location, + inboxSessionId: inboxSessionId)) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackInAppClick(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppClickRequest(inAppMessageContext: InAppMessageContext.from(message: message, + location: location, + inboxSessionId: inboxSessionId), + clickedUrl: clickedUrl) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackInAppClose(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + source: InAppCloseSource?, + clickedUrl: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppCloseRequest(inAppMessageContext: InAppMessageContext.from(message: message, + location: location, + inboxSessionId: inboxSessionId), + source: source, + clickedUrl: clickedUrl) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func track(inboxSession: IterableInboxSession, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInboxSessionRequest(inboxSession: inboxSession) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppDeliveryRequest(inAppMessageContext: InAppMessageContext.from(message: message, + location: nil)) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createInAppConsumeRequest(messageId) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation, + source: InAppDeleteSource?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppConsumeRequest(inAppMessageContext: InAppMessageContext.from(message: message, location: location), + source: source) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + // MARK: DEPRECATED + + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppOpenRequest(messageId) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppClickRequest(messageId, clickedUrl: clickedUrl) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) + } + + private let apiKey: String + private weak var authProvider: AuthProvider? + private weak var authManager: IterableInternalAuthManagerProtocol? + private let endPoint: String + private let deviceMetadata: DeviceMetadata + private let notificationListener: NotificationListener + private let taskScheduler: IterableTaskScheduler + private let taskRunner: IterableTaskRunner + + private func createRequestCreator(authProvider: AuthProvider) -> RequestCreator { + return RequestCreator(apiKey: apiKey, auth: authProvider.auth, deviceMetadata: deviceMetadata) + } + + private func sendIterableRequest(requestGenerator: (RequestCreator) -> Result, + successHandler onSuccess: OnSuccessHandler?, + failureHandler onFailure: OnFailureHandler?, + identifier: String) -> Future { + guard let authProvider = authProvider else { + fatalError("authProvider is missing") + } + + let requestCreator = createRequestCreator(authProvider: authProvider) + guard case let Result.success(iterableRequest) = requestGenerator(requestCreator) else { + return SendRequestError.createErroredFuture(reason: "Could not create request") + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: endPoint, + auth: authProvider.auth, + deviceMetadata: deviceMetadata, + iterableRequest: iterableRequest) + switch taskScheduler.schedule(apiCallRequest: apiCallRequest, context: IterableTaskContext(blocking: true)) { + case .success(let taskId): + let result = notificationListener.futureFromTask(withTaskId: taskId) + return RequestProcessorUtil.apply(successHandler: onSuccess, + andFailureHandler: onFailure, + andAuthManager: authManager, + toResult: result, + withIdentifier: identifier) + case .failure(let error): + ITBError(error.localizedDescription) + return SendRequestError.createErroredFuture(reason: error.localizedDescription) + } + } + + private class NotificationListener: NSObject { + init(notificationCenter: NotificationCenterProtocol) { + ITBInfo("OfflineRequestProcessor.NotificationListener.init()") + self.notificationCenter = notificationCenter + super.init() + self.notificationCenter.addObserver(self, + selector: #selector(onTaskFinishedWithSuccess(notification:)), + name: .iterableTaskFinishedWithSuccess, object: nil) + self.notificationCenter.addObserver(self, + selector: #selector(onTaskFinishedWithNoRetry(notification:)), + name: .iterableTaskFinishedWithNoRetry, object: nil) + } + + deinit { + ITBInfo("OfflineRequestProcessor.NotificationListener.deinit()") + self.notificationCenter.removeObserver(self) + } + + func futureFromTask(withTaskId taskId: String) -> Future { + ITBInfo() + let result = Promise() + pendingTasksMap[taskId] = result + return result + } + + @objc + private func onTaskFinishedWithSuccess(notification: Notification) { + ITBInfo() + if let taskSendRequestValue = IterableNotificationUtil.notificationToTaskSendRequestValue(notification) { + let taskId = taskSendRequestValue.taskId + ITBInfo("task: \(taskId) finished with success") + if let promise = pendingTasksMap[taskId] { + promise.resolve(with: taskSendRequestValue.sendRequestValue) + pendingTasksMap.removeValue(forKey: taskId) + } else { + ITBError("could not find promise for taskId: \(taskId)") + } + } else { + ITBError("Could not find taskId for notification") + } + } + + @objc + private func onTaskFinishedWithNoRetry(notification: Notification) { + ITBInfo() + if let taskSendRequestError = IterableNotificationUtil.notificationToTaskSendRequestError(notification) { + let taskId = taskSendRequestError.taskId + ITBInfo("task: \(taskId) finished with no retry") + if let promise = pendingTasksMap[taskId] { + promise.reject(with: taskSendRequestError.sendRequestError) + pendingTasksMap.removeValue(forKey: taskId) + } else { + ITBError("could not find promise for taskId: \(taskId)") + } + } else { + ITBError("Could not find taskId for notification") + } + } + + private let notificationCenter: NotificationCenterProtocol + private var pendingTasksMap = [String: Promise]() + } +} diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift new file mode 100644 index 000000000..a45c9c15b --- /dev/null +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -0,0 +1,285 @@ +// +// Created by Tapash Majumder on 8/10/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +/// `IterableAPIinternal` will delegate all network related calls to this struct. +struct OnlineRequestProcessor: RequestProcessorProtocol { + init(apiKey: String, + authProvider: AuthProvider?, + authManager: IterableInternalAuthManagerProtocol?, + endPoint: String, + networkSession: NetworkSessionProtocol, + deviceMetadata: DeviceMetadata) { + self.authManager = authManager + apiClient = ApiClient(apiKey: apiKey, + authProvider: authProvider, + endPoint: endPoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } + + func start() { + ITBInfo() + } + + func stop() { + ITBInfo() + } + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + self.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: notificationStateProvider.notificationsEnabled, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + } + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + } + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "updateUser", + forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + } + + @discardableResult + func updateEmail(_ newEmail: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "updateEmail", + forResult: apiClient.updateEmail(newEmail: newEmail)) + } + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackPurchase", + forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) + } + + @discardableResult + func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackPushOpen", + forResult: apiClient.track(pushOpen: campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields)) + } + + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackEvent", + forResult: apiClient.track(event: event, dataFields: dataFields)) + } + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "updateSubscriptions", + forResult: apiClient.updateSubscriptions(info.emailListIds, + unsubscribedChannelIds: info.unsubscribedChannelIds, + unsubscribedMessageTypeIds: info.unsubscribedMessageTypeIds, + subscribedMessageTypeIds: info.subscribedMessageTypeIds, + campaignId: info.campaignId, + templateId: info.templateId)) + } + + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) + } + + @discardableResult + func trackInAppClick(_ message: IterableInAppMessage, + location: InAppLocation = .inApp, + inboxSessionId: String? = nil, + clickedUrl: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inAppClick: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), + clickedUrl: clickedUrl) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: result) + } + + @discardableResult + func trackInAppClose(_ message: IterableInAppMessage, + location: InAppLocation = .inApp, + inboxSessionId: String? = nil, + source: InAppCloseSource? = nil, + clickedUrl: String? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inAppClose: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), + source: source, + clickedUrl: clickedUrl) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClose", + forResult: result) + } + + @discardableResult + func track(inboxSession: IterableInboxSession, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inboxSession: inboxSession) + + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInboxSession", + forResult: result) + } + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppDelivery", + forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) + } + + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "inAppConsume", + forResult: apiClient.inAppConsume(messageId: messageId)) + } + + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation = .inApp, + source: InAppDeleteSource? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.inAppConsume(inAppMessageContext: InAppMessageContext.from(message: message, location: location), + source: source) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "inAppConsumeWithSource", + forResult: result) + } + + // MARK: DEPRECATED + + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inAppOpen: messageId) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + } + + private let apiClient: ApiClientProtocol + private weak var authManager: IterableInternalAuthManagerProtocol? + + @discardableResult + private func register(registerTokenInfo: RegisterTokenInfo, + notificationsEnabled: Bool, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "registerToken", + forResult: apiClient.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: notificationsEnabled)) + } + + @discardableResult + private func disableDevice(forAllUsers allUsers: Bool, + hexToken: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + } + + private func applyCallbacks(successHandler onSuccess: OnSuccessHandler? = nil, + andFailureHandler onFailure: OnFailureHandler? = nil, + withIdentifier identifier: String, + forResult result: Future) -> Future { + RequestProcessorUtil.apply(successHandler: onSuccess, + andFailureHandler: onFailure, + andAuthManager: authManager, + toResult: result, + withIdentifier: identifier) + } +} diff --git a/swift-sdk/Internal/PersistenceHelper.swift b/swift-sdk/Internal/PersistenceHelper.swift new file mode 100644 index 000000000..53def6462 --- /dev/null +++ b/swift-sdk/Internal/PersistenceHelper.swift @@ -0,0 +1,44 @@ +// +// Created by Tapash Majumder on 7/22/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct PersistenceHelper { + static func task(from: IterableTaskManagedObject) -> IterableTask { + IterableTask(id: from.id, + name: from.name, + version: Int(from.version), + createdAt: from.createdAt, + modifiedAt: from.modifiedAt, + type: IterableTaskType(rawValue: from.type) ?? .apiCall, + attempts: Int(from.attempts), + lastAttemptedAt: from.lastAttemptedAt, + processing: from.processing, + scheduledAt: from.scheduledAt, + data: from.data, + failed: from.failed, + blocking: from.blocking, + requestedAt: from.requestedAt, + taskFailureData: from.taskFailureData) + } + + static func copy(from: IterableTask, to: IterableTaskManagedObject) { + to.id = from.id + to.name = from.name + to.version = Int64(from.version) + to.createdAt = from.createdAt + to.modifiedAt = from.modifiedAt + to.type = from.type.rawValue + to.attempts = Int64(from.attempts) + to.lastAttemptedAt = from.lastAttemptedAt + to.processing = from.processing + to.scheduledAt = from.scheduledAt + to.data = from.data + to.failed = from.failed + to.blocking = from.blocking + to.requestedAt = from.requestedAt + to.taskFailureData = from.taskFailureData + } +} diff --git a/swift-sdk/Internal/Promise.swift b/swift-sdk/Internal/Promise.swift index 837c440d2..ed4b47988 100644 --- a/swift-sdk/Internal/Promise.swift +++ b/swift-sdk/Internal/Promise.swift @@ -10,10 +10,10 @@ enum IterableError: Error { } extension IterableError: LocalizedError { - public var localizedDescription: String { + var errorDescription: String? { switch self { case let .general(description): - return description + return NSLocalizedString(description, comment: "error description") } } } @@ -132,6 +132,20 @@ extension Future { return promise } + + func replaceError(with defaultForError: Value) -> Future { + let promise = Promise() + + onSuccess { value in + promise.resolve(with: value) + } + + onError { _ in + promise.resolve(with: defaultForError) + } + + return promise + } } // This class takes the responsibility of setting value for Future diff --git a/swift-sdk/Internal/RequestCreator.swift b/swift-sdk/Internal/RequestCreator.swift index e3fb6733d..5b1f24e13 100644 --- a/swift-sdk/Internal/RequestCreator.swift +++ b/swift-sdk/Internal/RequestCreator.swift @@ -6,24 +6,6 @@ import Foundation import UIKit -// These are Iterable specific Request items. -// They don't have Api endpoint and request endpoint defined yet. -enum IterableRequest { - case get(GetRequest) - case post(PostRequest) -} - -struct GetRequest { - let path: String - let args: [String: String]? -} - -struct PostRequest { - let path: String - let args: [String: String]? - let body: [AnyHashable: Any]? -} - // This is a stateless pure functional class // This will create IterableRequest // The API Endpoint and request endpoint is not defined yet @@ -49,29 +31,25 @@ struct RequestCreator { return .success(.post(createPostRequest(path: Const.Path.updateEmail, body: body))) } - func createRegisterTokenRequest(hexToken: String, - appName: String, - deviceId: String, - sdkVersion: String?, - deviceAttributes: [String: String], - pushServicePlatform: String, + func createRegisterTokenRequest(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Result { guard let keyValueForCurrentUser = keyValueForCurrentUser else { ITBError("Both email and userId are nil") return .failure(IterableError.general(description: "Both email and userId are nil")) } - let dataFields = DataFieldsHelper.createDataFields(sdkVersion: sdkVersion, - deviceId: deviceId, + let dataFields = DataFieldsHelper.createDataFields(sdkVersion: registerTokenInfo.sdkVersion, + deviceId: registerTokenInfo.deviceId, device: UIDevice.current, bundle: Bundle.main, notificationsEnabled: notificationsEnabled, - deviceAttributes: deviceAttributes) + deviceAttributes: registerTokenInfo.deviceAttributes) let deviceDictionary: [String: Any] = [ - JsonKey.token.jsonKey: hexToken, - JsonKey.platform.jsonKey: pushServicePlatform, - JsonKey.applicationName.jsonKey: appName, + JsonKey.token.jsonKey: registerTokenInfo.hexToken, + JsonKey.platform.jsonKey: RequestCreator.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, + apnsType: registerTokenInfo.apnsType), + JsonKey.applicationName.jsonKey: registerTokenInfo.appName, JsonKey.dataFields.jsonKey: dataFields, ] @@ -462,6 +440,17 @@ struct RequestCreator { return JsonValue.DeviceIdiom.unspecified } } + + private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { + switch pushServicePlatform { + case .production: + return JsonValue.apnsProduction.jsonStringValue + case .sandbox: + return JsonValue.apnsSandbox.jsonStringValue + case .auto: + return apnsType == .sandbox ? JsonValue.apnsSandbox.jsonStringValue : JsonValue.apnsProduction.jsonStringValue + } + } } // MARK: - DEPRECATED diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift new file mode 100644 index 000000000..3eb3aefcd --- /dev/null +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -0,0 +1,274 @@ +// +// Created by Tapash Majumder on 8/24/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +protocol RequestProcessorStrategy { + var chooseOfflineProcessor: Bool { get } +} + +struct DefaultRequestProcessorStrategy: RequestProcessorStrategy { + let selectOffline: Bool + + var chooseOfflineProcessor: Bool { + selectOffline + } +} + +@available(iOS 10.0, *) +class RequestProcessor: RequestProcessorProtocol { + init(onlineCreator: @escaping () -> OnlineRequestProcessor, + offlineCreator: @escaping () -> OfflineRequestProcessor?, + strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { + ITBInfo() + self.onlineCreator = onlineCreator + self.offlineCreator = offlineCreator + self.strategy = strategy + } + + deinit { + ITBInfo() + } + + func start() { + ITBInfo() + chooseRequestProcessor().start() + } + + func stop() { + ITBInfo() + chooseRequestProcessor().stop() + } + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().register(registerTokenInfo: registerTokenInfo, + notificationStateProvider: notificationStateProvider, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().disableDeviceForCurrentUser(hexToken: hexToken, + withOnSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().disableDeviceForAllUsers(hexToken: hexToken, + withOnSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().updateUser(dataFields, + mergeNestedObjects: mergeNestedObjects, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func updateEmail(_ newEmail: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().updateEmail(newEmail, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackPurchase(total, + items: items, + dataFields: dataFields, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackPushOpen(campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().track(event: event, + dataFields: dataFields, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().updateSubscriptions(info: info, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackInAppOpen(message, + location: location, + inboxSessionId: inboxSessionId, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackInAppClick(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackInAppClick(message, + location: location, + inboxSessionId: inboxSessionId, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackInAppClose(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + source: InAppCloseSource?, + clickedUrl: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackInAppClose(message, + location: location, + inboxSessionId: inboxSessionId, + source: source, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func track(inboxSession: IterableInboxSession, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().track(inboxSession: inboxSession, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().track(inAppDelivery: message, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().inAppConsume(messageId, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation, + source: InAppDeleteSource?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().inAppConsume(message: message, + location: location, + source: source, + onSuccess: onSuccess, + onFailure: onFailure) + } + + // MARK: DEPRECATED + + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackInAppOpen(messageId, + onSuccess: onSuccess, + onFailure: onFailure) + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().trackInAppClick(messageId, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) + } + + private let onlineCreator: () -> OnlineRequestProcessor + private let offlineCreator: () -> OfflineRequestProcessor? + + private let strategy: RequestProcessorStrategy + + private lazy var offlineProcessor: OfflineRequestProcessor? = { + offlineCreator() + }() + + private lazy var onlineProcessor: OnlineRequestProcessor = { + onlineCreator() + }() + + private func chooseRequestProcessor() -> RequestProcessorProtocol { + if strategy.chooseOfflineProcessor { + if let offlineProcessor = self.offlineProcessor { + return offlineProcessor + } + return onlineProcessor + } else { + return onlineProcessor + } + } +} diff --git a/swift-sdk/Internal/RequestProcessorProtocol.swift b/swift-sdk/Internal/RequestProcessorProtocol.swift new file mode 100644 index 000000000..032550add --- /dev/null +++ b/swift-sdk/Internal/RequestProcessorProtocol.swift @@ -0,0 +1,144 @@ +// +// Created by Tapash Majumder on 8/10/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct RegisterTokenInfo { + let hexToken: String + let appName: String + let pushServicePlatform: PushServicePlatform + let apnsType: APNSType + let deviceId: String + let deviceAttributes: [String: String] + let sdkVersion: String? +} + +struct UpdateSubscriptionsInfo { + let emailListIds: [NSNumber]? + let unsubscribedChannelIds: [NSNumber]? + let unsubscribedMessageTypeIds: [NSNumber]? + let subscribedMessageTypeIds: [NSNumber]? + let campaignId: NSNumber? + let templateId: NSNumber? +} + +/// `IterableAPIinternal` will delegate all network related calls to this struct. +protocol RequestProcessorProtocol { + func start() + + func stop() + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func updateEmail(_ newEmail: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackInAppClick(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackInAppClose(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + source: InAppCloseSource?, + clickedUrl: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + @discardableResult + func track(inboxSession: IterableInboxSession, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation, + source: InAppDeleteSource?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + // MARK: DEPRECATED + + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future +} diff --git a/swift-sdk/Internal/RequestProcessorUtil.swift b/swift-sdk/Internal/RequestProcessorUtil.swift new file mode 100644 index 000000000..3160a4574 --- /dev/null +++ b/swift-sdk/Internal/RequestProcessorUtil.swift @@ -0,0 +1,60 @@ +// +// Created by Tapash Majumder on 8/26/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct RequestProcessorUtil { + @discardableResult + static func apply(successHandler onSuccess: OnSuccessHandler? = nil, + andFailureHandler onFailure: OnFailureHandler? = nil, + andAuthManager authManager: IterableInternalAuthManagerProtocol? = nil, + toResult result: Future, + withIdentifier identifier: String) -> Future { + result.onSuccess { json in + if let onSuccess = onSuccess { + onSuccess(json) + } else { + defaultOnSuccess(identifier)(json) + } + }.onError { error in + if error.httpStatusCode == 401, error.iterableCode == JsonValue.Code.invalidJwtPayload { + ITBError(error.reason) + authManager?.requestNewAuthToken(hasFailedPriorAuth: true, onSuccess: nil) + } else if error.httpStatusCode == 401, error.iterableCode == JsonValue.Code.badApiKey { + ITBError(error.reason) + } + + if let onFailure = onFailure { + onFailure(error.reason, error.data) + } else { + defaultOnFailure(identifier)(error.reason, error.data) + } + } + return result + } + + private static func defaultOnSuccess(_ identifier: String) -> OnSuccessHandler { + { data in + if let data = data { + ITBInfo("\(identifier) succeeded, got response: \(data)") + } else { + ITBInfo("\(identifier) succeeded.") + } + } + } + + private static func defaultOnFailure(_ identifier: String) -> OnFailureHandler { + { reason, data in + var toLog = "\(identifier) failed:" + if let reason = reason { + toLog += ", \(reason)" + } + if let data = data { + toLog += ", got response \(String(data: data, encoding: .utf8) ?? "nil")" + } + ITBError(toLog) + } + } +} diff --git a/swift-sdk/IterableConfig.swift b/swift-sdk/IterableConfig.swift index dc0ce83ce..b12bf13c3 100644 --- a/swift-sdk/IterableConfig.swift +++ b/swift-sdk/IterableConfig.swift @@ -128,6 +128,10 @@ public class IterableConfig: NSObject { /// will only apply if token-based authentication is enabled, and the current auth token has /// an expiration date field in it public var expiringAuthTokenRefreshPeriod: TimeInterval = 60.0 + + /// If set to true, events will be queued locally when network is offline. + /// When the network is online again, the queued events will be sent to our backend. + public var enableOfflineMode = false /// These are internal. Do not change internal var apiEndpoint = Endpoint.api diff --git a/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents new file mode 100644 index 000000000..718076578 --- /dev/null +++ b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/common/Common.swift b/tests/common/Common.swift index 4960c9aca..8c82918f9 100644 --- a/tests/common/Common.swift +++ b/tests/common/Common.swift @@ -36,6 +36,13 @@ struct InAppTestHelper { InAppMessageParser.parse(payload: payload).compactMap(parseResultToOptionalMessage) } + static func emptyInAppMessage(messageId: String = "message_id", + campaignId: NSNumber = NSNumber(value: 1)) -> IterableInAppMessage { + IterableInAppMessage(messageId: messageId, + campaignId: campaignId, + content: IterableHtmlInAppContent(edgeInsets: .zero, backgroundAlpha: 0.0, html: "")) + } + private static func parseResultToOptionalMessage(result: Result) -> IterableInAppMessage? { switch result { case .failure: diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index ebb3d9ee6..c0f0e9f01 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -9,6 +9,10 @@ import WebKit @testable import IterableSDK +class MockDateProvider: DateProviderProtocol { + var currentDate = Date() +} + @available(iOS 10.0, *) struct MockNotificationResponse: NotificationResponseProtocol { let userInfo: [AnyHashable: Any] @@ -105,18 +109,24 @@ public class MockPushTracker: NSObject, PushTrackerProtocol { public func trackPushOpen(_ userInfo: [AnyHashable: Any], dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) { + onFailure: OnFailureHandler?) -> Future { // save payload lastPushPayload = userInfo if let metadata = IterablePushNotificationMetadata.metadata(fromLaunchOptions: userInfo), metadata.isRealCampaignNotification() { - trackPushOpen(metadata.campaignId, templateId: metadata.templateId, messageId: metadata.messageId, appAlreadyRunning: false, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) + return trackPushOpen(metadata.campaignId, templateId: metadata.templateId, messageId: metadata.messageId, appAlreadyRunning: false, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) } else { - onFailure?("Not tracking push open - payload is not an Iterable notification, or a test/proof/ghost push", nil) + return SendRequestError.createErroredFuture(reason: "Not tracking push open - payload is not an Iterable notification, or a test/proof/ghost push") } } - public func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String, appAlreadyRunning: Bool, dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) { + public func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { self.campaignId = campaignId self.templateId = templateId self.messageId = messageId @@ -124,6 +134,8 @@ public class MockPushTracker: NSObject, PushTrackerProtocol { self.dataFields = dataFields self.onSuccess = onSuccess self.onFailure = onFailure + + return Promise(value: [:]) } } @@ -140,6 +152,31 @@ public class MockPushTracker: NSObject, PushTrackerProtocol { } class MockNetworkSession: NetworkSessionProtocol { + class MockDataTask: DataTaskProtocol { + init(url: URL, completionHandler: @escaping CompletionHandler, parent: MockNetworkSession) { + self.url = url + self.completionHandler = completionHandler + self.parent = parent + } + + var state: URLSessionDataTask.State = .suspended + + func resume() { + state = .running + parent.makeDataRequest(with: url, completionHandler: completionHandler) + } + + func cancel() { + canceled = true + state = .completed + } + + private let url: URL + private let completionHandler: CompletionHandler + private let parent: MockNetworkSession + private var canceled = false + } + var urlPatternDataMapping: [String: Data?]? var url: URL? var request: URLRequest? @@ -194,6 +231,11 @@ class MockNetworkSession: NetworkSessionProtocol { } } + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { + MockDataTask(url: url, completionHandler: completionHandler, parent: self) + } + + func getRequestBody() -> [AnyHashable: Any] { MockNetworkSession.json(fromData: request!.httpBody!) } @@ -236,6 +278,10 @@ class NoNetworkNetworkSession: NetworkSessionProtocol { completionHandler(try! JSONSerialization.data(withJSONObject: [:], options: []), response, error) } } + + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { + fatalError("Not implemented") + } } class MockInAppFetcher: InAppFetcherProtocol { @@ -343,39 +389,59 @@ class MockInAppDelegate: IterableInAppDelegate { class MockNotificationCenter: NotificationCenterProtocol { func addObserver(_ observer: Any, selector: Selector, name: Notification.Name?, object _: Any?) { - observers.append(Observer(observer: observer as! NSObject, notificationName: name!, selector: selector)) + observers.append(Observer(observer: observer as! NSObject, + notificationName: name!, + selector: selector)) } func removeObserver(_: Any) {} - func post(name: Notification.Name, object _: Any?, userInfo _: [AnyHashable: Any]?) { + func post(name: Notification.Name, object: Any?, userInfo: [AnyHashable: Any]?) { _ = observers.filter { $0.notificationName == name }.map { - _ = $0.observer.perform($0.selector, with: Notification(name: name)) + let notification = Notification(name: name, object: object, userInfo: userInfo) + _ = $0.observer.perform($0.selector, with: notification) } } - func addCallback(forNotification notification: Notification.Name, callback: @escaping () -> Void) { + @discardableResult + func addCallback(forNotification notification: Notification.Name, callback: @escaping (Notification) -> Void) -> String { class CallbackClass: NSObject { - let callback: () -> Void - init(callback: @escaping () -> Void) { + let callback: (Notification) -> Void + + init(callback: @escaping (Notification) -> Void) { self.callback = callback } - @objc func onNotification(notification _: Notification) { - callback() + @objc func onNotification(notification: Notification) { + callback(notification) } } - + + let id = IterableUtil.generateUUID() let callbackClass = CallbackClass(callback: callback) - addObserver(callbackClass, selector: #selector(callbackClass.onNotification(notification:)), name: notification, object: self) + + observers.append(Observer(id: id, + observer: callbackClass, + notificationName: notification, + selector: #selector(callbackClass.onNotification(notification:)))) + return id + } + + func removeCallbacks(withIds ids: String...) { + observers.removeAll { ids.contains($0.id) } } private class Observer: NSObject { + let id: String let observer: NSObject let notificationName: Notification.Name let selector: Selector - init(observer: NSObject, notificationName: Notification.Name, selector: Selector) { + init(id: String = IterableUtil.generateUUID(), + observer: NSObject, + notificationName: Notification.Name, + selector: Selector) { + self.id = id self.observer = observer self.notificationName = notificationName self.selector = selector diff --git a/tests/endpoint-tests/EndpointTests.swift b/tests/endpoint-tests/EndpointTests.swift index bf3c414d2..3e7d8ec06 100644 --- a/tests/endpoint-tests/EndpointTests.swift +++ b/tests/endpoint-tests/EndpointTests.swift @@ -23,7 +23,7 @@ class EndpointTests: XCTestCase { XCTFail() } - wait(for: [expectation1], timeout: 15) + wait(for: [expectation1], timeout: 60) } func test02UpdateEmail() throws { @@ -45,7 +45,7 @@ class EndpointTests: XCTestCase { }) { _, _ in XCTFail() } - wait(for: [expectation1, expectation2], timeout: 15) + wait(for: [expectation1, expectation2], timeout: 60) } func test03TrackPurchase() throws { diff --git a/tests/offline-events-tests/Info.plist b/tests/offline-events-tests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/tests/offline-events-tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift new file mode 100644 index 000000000..2fe4b453d --- /dev/null +++ b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift @@ -0,0 +1,64 @@ +// +// Created by Tapash Majumder on 9/8/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class NetworkConnectivityCheckerTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + } + + func testIsConnectedByDefault() throws { + let expectation1 = expectation(description: #function) + let checker = NetworkConnectivityChecker() + + checker.checkConnectivity().onSuccess { connected in + XCTAssertEqual(connected, true) + expectation1.fulfill() + } + + wait(for: [expectation1], timeout: 15.0) + } + + func testIsConnected() throws { + let expectation1 = expectation(description: #function) + let checker = NetworkConnectivityChecker(networkSession: MockNetworkSession()) + + checker.checkConnectivity().onSuccess { connected in + XCTAssertEqual(connected, true) + expectation1.fulfill() + } + + wait(for: [expectation1], timeout: 15.0) + } + + func testIsNotConnectedIfWrongStatus() throws { + let expectation1 = expectation(description: #function) + let checker = NetworkConnectivityChecker(networkSession: MockNetworkSession(statusCode: 300)) + + checker.checkConnectivity().onSuccess { connected in + XCTAssertEqual(connected, false) + expectation1.fulfill() + } + + wait(for: [expectation1], timeout: 15.0) + } + + func testIsNotConnectedIfError() throws { + let expectation1 = expectation(description: #function) + let checker = NetworkConnectivityChecker(networkSession: MockNetworkSession(statusCode: 200, data: Data(repeating: 1, count: 10), error: IterableError.general(description: "simulated error"))) + + checker.checkConnectivity().onSuccess { connected in + XCTAssertEqual(connected, false) + expectation1.fulfill() + } + + wait(for: [expectation1], timeout: 15.0) + } +} diff --git a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift new file mode 100644 index 000000000..cd36689d6 --- /dev/null +++ b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift @@ -0,0 +1,198 @@ +// +// Created by Tapash Majumder on 9/8/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class NetworkConnectivityManagerTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + } + + func testNetworkMonitor() throws { + let expectation1 = expectation(description: "do not fulfill before start") + expectation1.isInverted = true + let monitor = NetworkMonitor() + monitor.statusUpdatedCallback = { + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 1.0) + + let expectation2 = expectation(description: "fullfill when started") + monitor.statusUpdatedCallback = { + expectation2.fulfill() + } + monitor.start() + wait(for: [expectation2], timeout: 1.0) + + // now stop + monitor.stop() + let expectation3 = expectation(description: "don't fullfill when stopped") + expectation3.isInverted = true + monitor.statusUpdatedCallback = { + expectation3.fulfill() + } + wait(for: [expectation3], timeout: 1.0) + + let expectation4 = expectation(description: "fullfill when started again") + monitor.statusUpdatedCallback = { + expectation4.fulfill() + } + monitor.start() + wait(for: [expectation4], timeout: 1.0) + monitor.stop() + } + + func testPollingNetworkMonitor() throws { + let expectation1 = expectation(description: "do not fulfill before start") + expectation1.isInverted = true + let monitor = PollingNetworkMonitor(pollingInterval: 0.2) + monitor.statusUpdatedCallback = { + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 1.0) + + let expectation2 = expectation(description: "fullfill when started") + expectation2.expectedFulfillmentCount = 2 + monitor.statusUpdatedCallback = { + expectation2.fulfill() + } + monitor.start() + wait(for: [expectation2], timeout: 1.0) + + // now stop + monitor.stop() + let expectation3 = expectation(description: "don't fullfill when stopped") + expectation3.isInverted = true + monitor.statusUpdatedCallback = { + expectation3.fulfill() + } + wait(for: [expectation3], timeout: 1.0) + + let expectation4 = expectation(description: "fullfill when started again") + monitor.statusUpdatedCallback = { + expectation4.fulfill() + } + monitor.start() + wait(for: [expectation4], timeout: 1.0) + monitor.stop() + } + + func testConnectivityChange() throws { + let networkSession = MockNetworkSession() + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = PollingNetworkMonitor(pollingInterval: 0.5) + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter) + + // check online status before everything + XCTAssertTrue(manager.isOnline) + + // check that status is offline when there is network error + let expectation1 = expectation(description: "ConnectivityManager: check status change on network error") + networkSession.error = IterableError.general(description: "Mock error") + manager.connectivityChangedCallback = { connected in + XCTAssertFalse(connected) + expectation1.fulfill() + } + manager.start() + wait(for: [expectation1], timeout: 10.0) + + // check that status is online once error is removed + let expectation2 = expectation(description: "ConnectivityManager: check status change on network back to normal") + manager.connectivityChangedCallback = { connected in + XCTAssertTrue(connected) + expectation2.fulfill() + } + networkSession.error = nil + wait(for: [expectation2], timeout: 10.0) + + // check that status does not change once manager is stopped + let expectation3 = expectation(description: "ConnectivityManager: no status change when stopped") + expectation3.isInverted = true + manager.stop() + networkSession.error = IterableError.general(description: "Mock error") + manager.connectivityChangedCallback = { connected in + XCTAssertTrue(connected) + expectation3.fulfill() + } + wait(for: [expectation3], timeout: 1.0) + } + + func testOnlinePollingInterval() throws { + // Network status will never be updated + class NoUpdateNetworkMonitor: NetworkMonitorProtocol { + func start() {} + + func stop() {} + + var statusUpdatedCallback: (() -> Void)? + } + + let networkSession = MockNetworkSession() + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = NoUpdateNetworkMonitor() + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter, + onlineModePollingInterval: 0.5) + + // check online status before everything + XCTAssertTrue(manager.isOnline) + manager.start() + + // check that status is updated when status is offline + let expectation1 = expectation(description: "ConnectivityManager: check status change on network offline") + manager.connectivityChangedCallback = { connected in + XCTAssertFalse(connected) + expectation1.fulfill() + } + networkSession.error = IterableError.general(description: "Mock error") + + wait(for: [expectation1], timeout: 10.0) + } + + func testOfflinePollingInterval() throws { + // Network status will never be updated + class NoUpdateNetworkMonitor: NetworkMonitorProtocol { + func start() {} + + func stop() {} + + var statusUpdatedCallback: (() -> Void)? + } + + let networkSession = MockNetworkSession() + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = NoUpdateNetworkMonitor() + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter, + offlineModePollingInterval: 0.5) + + // check online status before everything + XCTAssertTrue(manager.isOnline) + manager.start() + + notificationCenter.post(name: .iterableNetworkOffline, object: nil, userInfo: nil) + XCTAssertFalse(manager.isOnline) + + // check that status is updated when status is online + let expectation1 = expectation(description: "ConnectivityManager: check status change on network online") + manager.connectivityChangedCallback = { connected in + XCTAssertTrue(connected) + expectation1.fulfill() + } + + wait(for: [expectation1], timeout: 10.0) + } +} diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift new file mode 100644 index 000000000..f8a0b90c7 --- /dev/null +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -0,0 +1,824 @@ +// +// Created by Tapash Majumder on 8/24/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class RequestProcessorTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + try! persistenceContextProvider.mainQueueContext().deleteAllTasks() + try! persistenceContextProvider.mainQueueContext().save() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testRegister() throws { + let registerTokenInfo = RegisterTokenInfo(hexToken: "zee-token", + appName: "zee-app-name", + pushServicePlatform: .auto, + apnsType: .sandbox, + deviceId: "deviceId", + deviceAttributes: [:], + sdkVersion: "6.x.x") + + let device = UIDevice.current + let dataFields: [String: Any] = [ + "deviceId": registerTokenInfo.deviceId, + "iterableSdkVersion": registerTokenInfo.sdkVersion!, + "notificationsEnabled": true, + "appPackageName": Bundle.main.appPackageName!, + "appVersion": Bundle.main.appVersion!, + "appBuild": Bundle.main.appBuild!, + "localizedModel": device.localizedModel, + "userInterfaceIdiom": "Phone", + "systemName": device.systemName, + "systemVersion": device.systemVersion, + "model": device.model, + "identifierForVendor": device.identifierForVendor!.uuidString + ] + let deviceDict: [String: Any] = [ + "token": registerTokenInfo.hexToken, + "applicationName": registerTokenInfo.appName, + "platform": "APNS_SANDBOX", + "dataFields": dataFields + ] + let bodyDict: [String: Any] = [ + "device": deviceDict, + "email": "user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.register(registerTokenInfo: registerTokenInfo, + notificationStateProvider: MockNotificationStateProvider(enabled: true), + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.registerDeviceToken, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testDisableUserforCurrentUser() throws { + let hexToken = "zee-token" + let bodyDict: [String: Any] = [ + "token": hexToken, + "email": "user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.disableDeviceForCurrentUser(hexToken: hexToken, + withOnSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.disableDevice, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testDisableUserforAllUsers() throws { + let hexToken = "zee-token" + let bodyDict: [String: Any] = [ + "token": hexToken, + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.disableDeviceForAllUsers(hexToken: hexToken, + withOnSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.disableDevice, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testUpdateUser() throws { + let dataFields = ["var1": "val1", "var2": "val2"] + let bodyDict: [String: Any] = [ + "dataFields": dataFields, + "email": "user@example.com", + "mergeNestedObjects": true + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateUser(dataFields, + mergeNestedObjects: true, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateUser, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testUpdateEmail() throws { + let bodyDict: [String: Any] = [ + "currentEmail": "user@example.com", + "newEmail": "new_user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateEmail("new_user@example.com", + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateEmail, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackPurchase() throws { + let total = NSNumber(value: 15.32) + let items = [CommerceItem(id: "id1", name: "myCommerceItem", price: 5.1, quantity: 2)] + let dataFields = ["var1": "val1", "var2": "val2"] + + let bodyDict: [String: Any] = [ + "items": [[ + "id": items[0].id, + "name": items[0].name, + "price": items[0].price, + "quantity": items[0].quantity, + ]], + "total": total, + "dataFields": dataFields, + "user": [ + "email": "user@example.com", + ], + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackPurchase(total, + items: items, + dataFields: dataFields, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackPurchase, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackPushOpen() throws { + let campaignId = 1 + let templateId = 2 + let messageId = "message_id" + let appAlreadyRunning = true + let dataFields: [String: Any] = [ + "var1": "val1", + "var2": "val2", + ] + var bodyDataFields = dataFields + bodyDataFields["appAlreadyRunning"] = appAlreadyRunning + let bodyDict: [String: Any] = [ + "dataFields": bodyDataFields, + "campaignId": campaignId, + "templateId": templateId, + "messageId": messageId, + "email": "user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackPushOpen(NSNumber(value: campaignId), + templateId: NSNumber(value: templateId), + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackPushOpen, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackEvent() throws { + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + let bodyDict: [String: Any] = [ + "eventName": eventName, + "dataFields": dataFields, + "email": "user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(event: eventName, + dataFields: dataFields, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackEvent, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testUpdateSubscriptions() throws { + let info = UpdateSubscriptionsInfo(emailListIds: [123], + unsubscribedChannelIds: [456], + unsubscribedMessageTypeIds: [789], + subscribedMessageTypeIds: [111], + campaignId: 1, + templateId: 2) + let bodyDict: [String: Any] = [ + "emailListIds": info.emailListIds!, + "unsubscribedChannelIds": info.unsubscribedChannelIds!, + "unsubscribedMessageTypeIds": info.unsubscribedMessageTypeIds!, + "subscribedMessageTypeIds": info.subscribedMessageTypeIds!, + "campaignId": info.campaignId!, + "templateId": info.templateId!, + "email": "user@example.com" + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateSubscriptions(info: info, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateSubscriptions, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppOpen() throws { + let messageId = "message_id" + let message = InAppTestHelper.emptyInAppMessage(messageId: messageId) + let inboxSessionId = "ibx1" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "inboxSessionId": inboxSessionId, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppOpen(message, + location: .inApp, + inboxSessionId: inboxSessionId, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppOpen, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppClick() throws { + let messageId = "message_id" + let message = InAppTestHelper.emptyInAppMessage(messageId: messageId) + let inboxSessionId = "ibx1" + let clickedUrl = "https://somewhere.com" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "inboxSessionId": inboxSessionId, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "inbox", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClick(message, + location: .inbox, + inboxSessionId: inboxSessionId, + clickedUrl: clickedUrl, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClick, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppClose() throws { + let messageId = "message_id" + let message = InAppTestHelper.emptyInAppMessage(messageId: messageId) + let inboxSessionId = "ibx1" + let clickedUrl = "https://closeme.com" + let closeSource = InAppCloseSource.link + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "inboxSessionId": inboxSessionId, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "inbox", + "saveToInbox": false, + "silentInbox": false, + ], + "closeAction": "link", + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClose(message, location: .inbox, + inboxSessionId: inboxSessionId, + source: closeSource, + clickedUrl: clickedUrl, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClose, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInboxSession() throws { + let inboxSessionId = IterableUtil.generateUUID() + let startDate = dateProvider.currentDate + let endDate = startDate.addingTimeInterval(60 * 5) + let impressions = [ + IterableInboxImpression(messageId: "message1", silentInbox: true, displayCount: 2, displayDuration: 1.23), + IterableInboxImpression(messageId: "message2", silentInbox: false, displayCount: 3, displayDuration: 2.34), + ] + let inboxSession = IterableInboxSession(id: inboxSessionId, + sessionStartTime: startDate, + sessionEndTime: endDate, + startTotalMessageCount: 15, + startUnreadMessageCount: 5, + endTotalMessageCount: 10, + endUnreadMessageCount: 3, + impressions: impressions) + + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "inboxSessionId": inboxSessionId, + "inboxSessionStart": IterableUtil.int(fromDate: startDate), + "inboxSessionEnd": IterableUtil.int(fromDate: endDate), + "startTotalMessageCount": inboxSession.startTotalMessageCount, + "startUnreadMessageCount": inboxSession.startUnreadMessageCount, + "endTotalMessageCount": inboxSession.endTotalMessageCount, + "endUnreadMessageCount": inboxSession.endUnreadMessageCount, + "impressions": impressions.compactMap { $0.asDictionary() }, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(inboxSession: inboxSession, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInboxSession, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppDelivery() throws { + let messageId = "message_id" + let message = InAppTestHelper.emptyInAppMessage(messageId: messageId) + + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "messageContext": [ + "saveToInbox": false, + "silentInbox": false, + ], + "deviceInfo": Self.deviceMetadata.asDictionary()!, + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(inAppDelivery: message, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppDelivery, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppConsume() throws { + let messageId = "message_id" + + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.inAppConsume(messageId, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.inAppConsume, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppConsume2() throws { + let messageId = "message_id" + let message = InAppTestHelper.emptyInAppMessage(messageId: messageId) + let location = InAppLocation.inbox + let source = InAppDeleteSource.deleteButton + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "messageContext": [ + "location": "inbox", + "saveToInbox": false, + "silentInbox": false, + ], + "deleteAction": "delete-button", + "deviceInfo": Self.deviceMetadata.asDictionary()!, + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.inAppConsume(message: message, + location: location, + source: source, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.inAppConsume, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppOpen2() throws { + let messageId = "message_id" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppOpen(messageId, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppOpen, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + func testTrackInAppClick2() throws { + let messageId = "message_id" + let clickedUrl = "https://somewhere.com" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "deviceInfo": Self.deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let expectations = createExpectations(description: #function) + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClick(messageId, + clickedUrl: clickedUrl, + onSuccess: expectations.onSuccess, + onFailure: expectations.onFailure) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClick, + bodyDict: bodyDict) + + wait(for: [expectations.successExpectation, expectations.failureExpectation], timeout: 15.0) + } + + private func processRequestWithSuccessAndFailure(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) throws { + + processOnlineRequestWithSuccess(requestGenerator: requestGenerator, + path: path, + bodyDict: bodyDict) + processOnlineRequestWithFailure(requestGenerator: requestGenerator, + path: path, + bodyDict: bodyDict) + processOfflineRequestWithSuccess(requestGenerator: requestGenerator, + path: path, + bodyDict: bodyDict) + processOfflineRequestWithFailure(requestGenerator: requestGenerator, + path: path, + bodyDict: bodyDict) + } + + private func processOnlineRequestWithSuccess(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) { + let notificationCenter = MockNotificationCenter() + let networkSession = MockNetworkSession() + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + let requestProcessor = createRequestProcessor(networkSession: networkSession, + notificationCenter: notificationCenter, + selectOffline: false) + let request = { requestGenerator(requestProcessor) } + let expectation1 = expectation(description: #function) + processRequestWithSuccess(request: request, + networkSession: networkSession, + path: path, + bodyDict: bodyDict, + expectation: expectation1) + wait(for: [expectation1], timeout: 15.0) + } + + private func processOnlineRequestWithFailure(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) { + let notificationCenter = MockNotificationCenter() + let networkSession = MockNetworkSession(statusCode: 400) + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + let requestProcessor = createRequestProcessor(networkSession: networkSession, + notificationCenter: notificationCenter, + selectOffline: false) + let request = { requestGenerator(requestProcessor) } + let expectation1 = expectation(description: #function) + processRequestWithFailure(request: request, + networkSession: networkSession, + path: path, + bodyDict: bodyDict, + expectation: expectation1) + wait(for: [expectation1], timeout: 15.0) + } + + private func processOfflineRequestWithSuccess(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) { + let notificationCenter = MockNotificationCenter() + let networkSession = MockNetworkSession() + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + let requestProcessor = createRequestProcessor(networkSession: networkSession, + notificationCenter: notificationCenter, + selectOffline: true) + let request = { requestGenerator(requestProcessor) } + let expectation1 = expectation(description: #function) + processRequestWithSuccess(request: request, + networkSession: networkSession, + path: path, + bodyDict: bodyDict, + expectation: expectation1) + waitForTaskRunner(requestProcessor: requestProcessor, + expectation: expectation1) + } + + private func processOfflineRequestWithFailure(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) { + let notificationCenter = MockNotificationCenter() + let networkSession = MockNetworkSession(statusCode: 400) + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + let requestProcessor = createRequestProcessor(networkSession: networkSession, + notificationCenter: notificationCenter, + selectOffline: true) + let request = { requestGenerator(requestProcessor) } + let expectation1 = expectation(description: #function) + processRequestWithFailure(request: request, + networkSession: networkSession, + path: path, + bodyDict: bodyDict, + expectation: expectation1) + waitForTaskRunner(requestProcessor: requestProcessor, + expectation: expectation1) + } + + private func createRequestProcessor(networkSession: NetworkSessionProtocol, + notificationCenter: NotificationCenterProtocol, + selectOffline: Bool) -> RequestProcessorProtocol { + let taskScheduler = IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider) + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 0.5) + + return RequestProcessor(onlineCreator: { + OnlineRequestProcessor(apiKey: "zee-api-key", + authProvider: self, + authManager: nil, + endPoint: Endpoint.api, + networkSession: networkSession, + deviceMetadata: Self.deviceMetadata) }, + offlineCreator: { + OfflineRequestProcessor(apiKey: "zee-api-key", + authProvider: self, + authManager: nil, + endPoint: Endpoint.api, + deviceMetadata: Self.deviceMetadata, + taskScheduler: taskScheduler, + taskRunner: taskRunner, + notificationCenter: notificationCenter) }, + strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline)) + } + + private func processRequestWithSuccess(request: () -> Future, + networkSession: MockNetworkSession, + path: String, + bodyDict: [AnyHashable: Any], + expectation: XCTestExpectation) { + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + + request().onSuccess { json in + expectation.fulfill() + }.onError { error in + XCTFail() + } + } + + private func processRequestWithFailure(request: () -> Future, + networkSession: MockNetworkSession, + path: String, + bodyDict: [AnyHashable: Any], + expectation: XCTestExpectation) { + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + request().onSuccess { json in + XCTFail() + }.onError { error in + expectation.fulfill() + } + } + + private func waitForTaskRunner(requestProcessor: RequestProcessorProtocol, + expectation: XCTestExpectation) { + requestProcessor.start() + wait(for: [expectation], timeout: 15.0) + requestProcessor.stop() + } + + struct Exp { + let successExpectation: XCTestExpectation + let onSuccess: OnSuccessHandler + let failureExpectation: XCTestExpectation + let onFailure: OnFailureHandler + } + + private func createExpectations(description: String) -> Exp { + let (successExpectation, onSuccess) = createSuccessExpectation(description: "success: \(description)") + let (failureExpectation, onFailure) = createFailureExpectation(description: "failure: \(description)") + return Exp(successExpectation: successExpectation, + onSuccess: onSuccess, + failureExpectation: failureExpectation, + onFailure: onFailure) + } + + private func createSuccessExpectation(description: String) -> (XCTestExpectation, OnSuccessHandler) { + let expectation1 = expectation(description: description) + expectation1.expectedFulfillmentCount = 2 + let onSuccess: OnSuccessHandler = { _ in + expectation1.fulfill() + } + return (expectation: expectation1, onSuccess: onSuccess) + } + + private func createFailureExpectation(description: String) -> (XCTestExpectation, OnFailureHandler) { + let expectation1 = expectation(description: description) + expectation1.expectedFulfillmentCount = 2 + let onFailure: OnFailureHandler = { _, _ in + expectation1.fulfill() + } + return (expectation: expectation1, onFailure: onFailure) + } + + private static let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") + + private let dateProvider = MockDateProvider() + + private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! + return provider + }() +} + +extension RequestProcessorTests: AuthProvider { + var auth: Auth { + Auth(userId: nil, email: "user@example.com", authToken: nil) + } +} diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift new file mode 100644 index 000000000..10f5fb8f8 --- /dev/null +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -0,0 +1,191 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class TaskProcessorTests: XCTestCase { + func testAPICallForTrackEventWithPersistence() throws { + let apiKey = "test-api-key" + let email = "user@example.com" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let expectation1 = expectation(description: #function) + let auth = Auth(userId: nil, email: email, authToken: nil) + let config = IterableConfig() + let networkSession = MockNetworkSession() + let internalAPI = IterableAPIInternal.initializeForTesting(apiKey: apiKey, config: config, networkSession: networkSession) + + let requestCreator = RequestCreator(apiKey: apiKey, + auth: auth, + deviceMetadata: internalAPI.deviceMetadata) + guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(eventName, dataFields: dataFields) else { + XCTFail("Could not create trackEvent request") + return + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: config.apiEndpoint, + auth: auth, + deviceMetadata: internalAPI.deviceMetadata, + iterableRequest: trackEventRequest) + let data = try JSONEncoder().encode(apiCallRequest) + + // persist data + let taskId = IterableUtil.generateUUID() + try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, + type: .apiCall, + scheduledAt: Date(), + data: data, + requestedAt: Date())) + try persistenceProvider.mainQueueContext().save() + + // load data + let found = try persistenceProvider.mainQueueContext().findTask(withId: taskId)! + + // process data + let processor = IterableAPICallTaskProcessor(networkSession: internalAPI.networkSession) + try processor.process(task: found).onSuccess { _ in + let body = networkSession.getRequestBody() as! [String: Any] + TestUtils.validateMatch(keyPath: KeyPath(.email), value: email, inDictionary: body) + TestUtils.validateMatch(keyPath: KeyPath(.dataFields), value: dataFields, inDictionary: body) + expectation1.fulfill() + } + + try persistenceProvider.mainQueueContext().delete(task: found) + try persistenceProvider.mainQueueContext().save() + + wait(for: [expectation1], timeout: 15.0) + } + + func testNetworkAvailable() throws { + let expectation1 = expectation(description: #function) + let task = try createSampleTask()! + + let networkSession = MockNetworkSession(statusCode: 200) + // process data + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + try processor.process(task: task) + .onSuccess { taskResult in + switch taskResult { + case .success(detail: _): + expectation1.fulfill() + case .failureWithNoRetry(detail: _): + XCTFail("not expecting failure with no retry") + case .failureWithRetry(retryAfter: _, detail: _): + XCTFail("not expecting failure with retry") + } + } + .onError { _ in + XCTFail() + } + + try persistenceProvider.mainQueueContext().delete(task: task) + try persistenceProvider.mainQueueContext().save() + wait(for: [expectation1], timeout: 15.0) + } + + func testNetworkUnavailable() throws { + let expectation1 = expectation(description: #function) + let task = try createSampleTask()! + + let networkError = IterableError.general(description: "The Internet connection appears to be offline.") + let networkSession = MockNetworkSession(statusCode: 0, data: nil, error: networkError) + // process data + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + try processor.process(task: task) + .onSuccess { taskResult in + switch taskResult { + case .success(detail: _): + XCTFail("not expecting success") + case .failureWithNoRetry(detail: _): + XCTFail("not expecting failure with no retry") + case .failureWithRetry(retryAfter: _, detail: _): + expectation1.fulfill() + } + } + .onError { _ in + XCTFail() + } + + try persistenceProvider.mainQueueContext().delete(task: task) + try persistenceProvider.mainQueueContext().save() + wait(for: [expectation1], timeout: 15.0) + } + + func testUnrecoverableError() throws { + let expectation1 = expectation(description: #function) + let task = try createSampleTask()! + + let networkSession = MockNetworkSession(statusCode: 401, data: nil, error: nil) + // process data + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + try processor.process(task: task) + .onSuccess { taskResult in + switch taskResult { + case .success(detail: _): + XCTFail("not expecting success") + case .failureWithNoRetry(detail: _): + expectation1.fulfill() + case .failureWithRetry(retryAfter: _, detail: _): + XCTFail("not expecting failure with retry") + } + } + .onError { _ in + XCTFail() + } + + try persistenceProvider.mainQueueContext().delete(task: task) + try persistenceProvider.mainQueueContext().save() + wait(for: [expectation1], timeout: 15.0) + } + + private func createSampleTask() throws -> IterableTask? { + let apiKey = "test-api-key" + let email = "user@example.com" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let auth = Auth(userId: nil, email: email, authToken: nil) + let requestCreator = RequestCreator(apiKey: apiKey, + auth: auth, + deviceMetadata: deviceMetadata) + guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(eventName, dataFields: dataFields) else { + XCTFail("Could not create trackEvent request") + return nil + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: Endpoint.api, + auth: auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + let data = try JSONEncoder().encode(apiCallRequest) + + // persist data + let taskId = IterableUtil.generateUUID() + try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, + type: .apiCall, + scheduledAt: Date(), + data: data, + requestedAt: Date())) + try persistenceProvider.mainQueueContext().save() + + return try persistenceProvider.mainQueueContext().findTask(withId: taskId) + } + + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") + + private lazy var persistenceProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider()! + try! provider.mainQueueContext().deleteAllTasks() + try! provider.mainQueueContext().save() + return provider + }() +} diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift new file mode 100644 index 000000000..986754f86 --- /dev/null +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -0,0 +1,279 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class TaskRunnerTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + try! persistenceContextProvider.mainQueueContext().deleteAllTasks() + try! persistenceContextProvider.mainQueueContext().save() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testMultipleTasksInSequence() throws { + let expectation1 = expectation(description: #function) + expectation1.expectedFulfillmentCount = 3 + + var scheduledTaskIds = [String]() + var taskIds = [String]() + let notificationCenter = MockNotificationCenter() + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { notification in + let taskSendRequestValue = IterableNotificationUtil.notificationToTaskSendRequestValue(notification)! + taskIds.append(taskSendRequestValue.taskId) + expectation1.fulfill() + } + + let taskRunner = IterableTaskRunner(networkSession: MockNetworkSession(), + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 0.5) + taskRunner.start() + + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + + wait(for: [expectation1], timeout: 15.0) + XCTAssertEqual(taskIds, scheduledTaskIds) + + XCTAssertEqual(try persistenceContextProvider.mainQueueContext().findAllTasks().count, 0) + taskRunner.stop() + } + + func testFailureWithRetry() throws { + let networkError = IterableError.general(description: "The Internet connection appears to be offline.") + let networkSession = MockNetworkSession(statusCode: 0, data: nil, error: networkError) + + var scheduledTaskIds = [String]() + var retryTaskIds = [String]() + let notificationCenter = MockNotificationCenter() + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithRetry) { notification in + let taskSendRequestError = IterableNotificationUtil.notificationToTaskSendRequestError(notification)! + if !retryTaskIds.contains(taskSendRequestError.taskId) { + retryTaskIds.append(taskSendRequestError.taskId) + } + } + + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 1.0) + taskRunner.start() + + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + + let predicate = NSPredicate { _, _ in + return retryTaskIds.count == 1 + } + let expectation2 = expectation(for: predicate, evaluatedWith: nil, handler: nil) + wait(for: [expectation2], timeout: 5.0) + XCTAssertEqual(scheduledTaskIds[0], retryTaskIds[0]) + + XCTAssertEqual(try persistenceContextProvider.mainQueueContext().findAllTasks().count, 3) + taskRunner.stop() + } + + func testFailureWithNoRetry() throws { + let networkSession = MockNetworkSession(statusCode: 401, data: nil, error: nil) + + let expectation1 = expectation(description: #function) + expectation1.expectedFulfillmentCount = 3 + + var scheduledTaskIds = [String]() + var failedTaskIds = [String]() + let notificationCenter = MockNotificationCenter() + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithNoRetry) { notification in + let taskSendRequestError = IterableNotificationUtil.notificationToTaskSendRequestError(notification)! + failedTaskIds.append(taskSendRequestError.taskId) + expectation1.fulfill() + } + + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 0.5) + taskRunner.start() + + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + scheduledTaskIds.append(try scheduleSampleTask(notificationCenter: notificationCenter)) + + wait(for: [expectation1], timeout: 15.0) + XCTAssertEqual(failedTaskIds, scheduledTaskIds) + + XCTAssertEqual(try persistenceContextProvider.mainQueueContext().findAllTasks().count, 0) + taskRunner.stop() + } + + func testDoNotRunWhenNetworkIsOffline() throws { + let networkSession = MockNetworkSession(statusCode: 401, data: nil, error: IterableError.general(description: "Mock error")) + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = PollingNetworkMonitor(pollingInterval: 0.2) + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter) + + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + connectivityManager: manager) + taskRunner.start() + + // Now schedule a task, giving it some time for task runner to be updated with + // offliine network status + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let _ = try! self.scheduleSampleTask(notificationCenter: notificationCenter) + } + + verifyNoTaskIsExecuted(notificationCenter, forInterval: 1.0) + + XCTAssertEqual(try persistenceContextProvider.mainQueueContext().findAllTasks().count, 1) + taskRunner.stop() + } + + func testResumeWhenNetworkIsBackOffline() throws { + let networkSession = MockNetworkSession(statusCode: 401, json: [:], error: IterableError.general(description: "Mock error")) + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = PollingNetworkMonitor(pollingInterval: 0.2) + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter) + + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + connectivityManager: manager) + taskRunner.start() + + // Now schedule a task, giving it some time for task runner to be updated with + // offliine network status + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let _ = try! self.scheduleSampleTask(notificationCenter: notificationCenter) + } + + verifyNoTaskIsExecuted(notificationCenter, forInterval: 1.0) + + // set network status back to normal + networkSession.statusCode = 200 + networkSession.error = nil + + verifyTaskIsExecuted(notificationCenter, withinInterval: 10.0) + + XCTAssertEqual(try persistenceContextProvider.mainQueueContext().findAllTasks().count, 0) + taskRunner.stop() + } + + func testForegroundBackgroundChange() throws { + let networkSession = MockNetworkSession() + let checker = NetworkConnectivityChecker(networkSession: networkSession) + let monitor = PollingNetworkMonitor(pollingInterval: 0.5) + let notificationCenter = MockNotificationCenter() + let manager = NetworkConnectivityManager(networkMonitor: monitor, + connectivityChecker: checker, + notificationCenter: notificationCenter) + + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 0.5, + connectivityManager: manager) + taskRunner.start() + + let _ = try! self.scheduleSampleTask(notificationCenter: notificationCenter) + verifyTaskIsExecuted(notificationCenter, withinInterval: 1.0) + + // Now move app to background + notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil, userInfo: nil) + // Now schedule a task, giving it some time for task runner to be updated with + // app background status + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let _ = try! self.scheduleSampleTask(notificationCenter: notificationCenter) + } + + verifyNoTaskIsExecuted(notificationCenter, forInterval: 1.0) + + // Now move app to foreground + notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil, userInfo: nil) + verifyTaskIsExecuted(notificationCenter, withinInterval: 10.0) + taskRunner.stop() + } + + private func scheduleSampleTask(notificationCenter: NotificationCenterProtocol) throws -> String { + let apiKey = "zee-api-key" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let requestCreator = RequestCreator(apiKey: apiKey, auth: auth, deviceMetadata: deviceMetadata) + guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(eventName, dataFields: dataFields) else { + throw IterableError.general(description: "Could not create trackEvent request") + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: Endpoint.api, + auth: auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + + return try IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider).schedule(apiCallRequest: apiCallRequest).get() + } + + private func verifyNoTaskIsExecuted(_ notificationCenter: MockNotificationCenter, forInterval interval: TimeInterval) { + let expectation1 = expectation(description: "Wait for task complete notification.") + expectation1.isInverted = true + + let id1 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithRetry) { _ in + XCTFail() + } + let id2 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithNoRetry) { _ in + XCTFail() + } + let id3 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + XCTFail() + } + wait(for: [expectation1], timeout: interval) + notificationCenter.removeCallbacks(withIds: id1, id2, id3) + } + + private func verifyTaskIsExecuted(_ notificationCenter: MockNotificationCenter, withinInterval interval: TimeInterval) { + let expectation1 = expectation(description: "Wait for task complete notification.") + let id1 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + expectation1.fulfill() + } + wait(for: [expectation1], timeout: interval) + notificationCenter.removeCallbacks(withIds: id1) + } + + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") + + private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! + return provider + }() + + private let dateProvider = MockDateProvider() +} + +extension TaskRunnerTests: AuthProvider { + var auth: Auth { + Auth(userId: nil, email: "user@example.com", authToken: nil) + } +} diff --git a/tests/offline-events-tests/TaskSchedulerTests.swift b/tests/offline-events-tests/TaskSchedulerTests.swift new file mode 100644 index 000000000..aac8363d8 --- /dev/null +++ b/tests/offline-events-tests/TaskSchedulerTests.swift @@ -0,0 +1,73 @@ +// +// Created by Tapash Majumder on 9/15/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class TaskSchedulerTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + try! persistenceContextProvider.mainQueueContext().deleteAllTasks() + try! persistenceContextProvider.mainQueueContext().save() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + } + + func testScheduleTask() throws { + let expectation1 = expectation(description: #function) + let apiKey = "zee-api-key" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let notificationCenter = MockNotificationCenter() + notificationCenter.addCallback(forNotification: .iterableTaskScheduled) { _ in + expectation1.fulfill() + } + let requestCreator = RequestCreator(apiKey: apiKey, auth: auth, deviceMetadata: deviceMetadata) + guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(eventName, dataFields: dataFields) else { + throw IterableError.general(description: "Could not create trackEvent request") + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: Endpoint.api, + auth: auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + + let scheduler = IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider) + let taskId = try scheduler.schedule(apiCallRequest: apiCallRequest).get() + + wait(for: [expectation1], timeout: 10.0) + + let found = try persistenceContextProvider.mainQueueContext().findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.name, Const.Path.trackEvent) + } + + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") + + private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! + return provider + }() + + private let dateProvider = MockDateProvider() +} + +extension TaskSchedulerTests: AuthProvider { + var auth: Auth { + Auth(userId: nil, email: "user@example.com", authToken: nil) + } +} diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift new file mode 100644 index 000000000..2f5c19d35 --- /dev/null +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -0,0 +1,166 @@ +// +// Created by Tapash Majumder on 7/22/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class TasksCRUDTests: XCTestCase { + func testCreate() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + let taskName = "zee task name" + let task = try createTask(context: context, id: taskId, name: taskName, type: .apiCall) + try context.save() + XCTAssertEqual(task.id, taskId) + XCTAssertEqual(task.type, .apiCall) + XCTAssertEqual(task.name, taskName) + + let newContext = persistenceProvider.mainQueueContext() + let found = try newContext.findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.type, .apiCall) + } + + func testUpdate() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + let task = try createTask(context: context, id: taskId, type: .apiCall) + try context.save() + + let attempts = 2 + let lastAttemptedAt = Date() + let processing = true + let scheduledAt = Date() + let data = Data(repeating: 1, count: 20) + let failed = true + let taskFailureData = Data(repeating: 2, count: 11) + let updatedTask = task.updated(attempts: attempts, + lastAttemptedAt: lastAttemptedAt, + processing: processing, + scheduledAt: scheduledAt, + data: data, + failed: failed, + taskFailureData: taskFailureData) + + try context.update(task: updatedTask) + try context.save() + + let newContext = persistenceProvider.mainQueueContext() + let found = try newContext.findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.version, task.version) + XCTAssertEqual(found.type, .apiCall) + XCTAssertNotNil(found.createdAt) + XCTAssertNotNil(found.modifiedAt) + XCTAssertEqual(found.attempts, attempts) + XCTAssertEqual(found.lastAttemptedAt, lastAttemptedAt) + XCTAssertEqual(found.processing, processing) + XCTAssertEqual(found.scheduledAt, scheduledAt) + XCTAssertEqual(found.data, data) + XCTAssertEqual(found.failed, failed) + XCTAssertEqual(found.blocking, task.blocking) + XCTAssertEqual(found.requestedAt, task.requestedAt) + XCTAssertEqual(found.taskFailureData, taskFailureData) + } + + func testDelete() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + try createTask(context: context, id: taskId, type: .apiCall) + try context.save() + + let newContext = persistenceProvider.mainQueueContext() + let found = try newContext.findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.type, .apiCall) + + try context.delete(task: found) + try context.save() + + XCTAssertNil(try newContext.findTask(withId: taskId)) + } + + func testFindNextTask() throws { + let context = persistenceProvider.newBackgroundContext() + try context.deleteAllTasks() + try context.save() + + let tasks = try context.findAllTasks() + XCTAssertEqual(tasks.count, 0) + + let date1 = Date() + let date2 = date1.advanced(by: 100) + let date3 = date2.advanced(by: 100) + + var dates = [date1, date2, date3] + dates.shuffle() + + for date in dates { + dateProvider.currentDate = date + let task = IterableTask(id: IterableUtil.generateUUID(), + type: .apiCall, + scheduledAt: date, + requestedAt: date) + try context.create(task: task) + } + + try context.save() + + var scheduledAtValues = [Date]() + while let nextTask = try context.nextTask() { + scheduledAtValues.append(nextTask.scheduledAt) + try context.delete(task: nextTask) + try context.save() + } + + XCTAssertEqual(scheduledAtValues.count, 3) + XCTAssertTrue(scheduledAtValues.isAscending()) + let allTasks = try context.findAllTasks() + XCTAssertEqual(allTasks.count, 0) + } + + func testFindAll() throws { + let context = persistenceProvider.newBackgroundContext() + try context.deleteAllTasks() + try context.save() + + let tasks = try context.findAllTasks() + XCTAssertEqual(tasks.count, 0) + + try createTask(context: context, id: IterableUtil.generateUUID(), type: .apiCall) + try createTask(context: context, id: IterableUtil.generateUUID(), type: .apiCall) + try context.save() + + let newTasks = try context.findAllTasks() + XCTAssertEqual(newTasks.count, 2) + + try context.deleteAllTasks() + try context.save() + } + + @discardableResult + private func createTask(context: IterablePersistenceContext, + id: String, + name: String? = nil, + type: IterableTaskType = .apiCall) throws -> IterableTask { + let template = IterableTask(id: id, + name: name, + type: type, + scheduledAt: dateProvider.currentDate, + requestedAt: dateProvider.currentDate) + return try context.create(task: template) + } + + private let dateProvider = MockDateProvider() + + private lazy var persistenceProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! + try! provider.mainQueueContext().deleteAllTasks() + try! provider.mainQueueContext().save() + return provider + }() +} + diff --git a/tests/swift-sdk-swift-tests/InAppHelperTests.swift b/tests/swift-sdk-swift-tests/InAppHelperTests.swift index 3292ae7f0..a3053dc82 100644 --- a/tests/swift-sdk-swift-tests/InAppHelperTests.swift +++ b/tests/swift-sdk-swift-tests/InAppHelperTests.swift @@ -95,12 +95,7 @@ class InAppHelperTests: XCTestCase { } private class MockApiClient: ApiClientProtocol { - func register(hexToken _: String, - appName _: String, - deviceId _: String, - sdkVersion _: String?, - deviceAttributes _: [String: String], - pushServicePlatform _: String, + func register(registerTokenInfo _: RegisterTokenInfo, notificationsEnabled _: Bool) -> Future { fatalError() } @@ -117,10 +112,6 @@ class InAppHelperTests: XCTestCase { fatalError() } - func track(pushOpen _: NSNumber, templateId _: NSNumber?, messageId _: String?, appAlreadyRunning _: Bool, dataFields _: [AnyHashable: Any]?) -> Future { - fatalError() - } - func track(pushOpen _: NSNumber, templateId _: NSNumber?, messageId _: String, appAlreadyRunning _: Bool, dataFields _: [AnyHashable: Any]?) -> Future { fatalError() } diff --git a/tests/swift-sdk-swift-tests/InAppTests.swift b/tests/swift-sdk-swift-tests/InAppTests.swift index 29c351dab..0eec58be5 100644 --- a/tests/swift-sdk-swift-tests/InAppTests.swift +++ b/tests/swift-sdk-swift-tests/InAppTests.swift @@ -69,24 +69,22 @@ class InAppTests: XCTestCase { expectation1.fulfill() } - var internalApi: IterableAPIInternal! let config = IterableConfig() let mockUrlDelegate = MockUrlDelegate(returnValue: true) mockUrlDelegate.callback = { _, _ in - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) expectation2.fulfill() } config.urlDelegate = mockUrlDelegate - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { [weak internalApi] _ in // first message has been processed by now - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) + XCTAssertEqual(internalApi?.inAppManager.getMessages().count, 0) expectation3.fulfill() } @@ -117,9 +115,9 @@ class InAppTests: XCTestCase { inAppDisplayer: mockInAppDisplayer ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { _ in - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 1) - XCTAssertEqual(internalApi.inAppManager.getMessages()[0].didProcessTrigger, true) + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { [weak internalApi] _ in + XCTAssertEqual(internalApi?.inAppManager.getMessages().count, 1) + XCTAssertEqual(internalApi?.inAppManager.getMessages()[0].didProcessTrigger, true) expectation2.fulfill() } @@ -206,7 +204,11 @@ class InAppTests: XCTestCase { inAppDisplayer: mockInAppDisplayer ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getMessages() XCTAssertEqual(messages.count, 3) XCTAssertEqual(Set(messages.map { $0.didProcessTrigger }), Set([true, true, true])) @@ -222,11 +224,9 @@ class InAppTests: XCTestCase { func testAutoShowInAppOpenUrlByDefault() { let expectation1 = expectation(description: "testAutoShowInAppOpenUrlByDefault") - var internalApi: IterableAPIInternal! let mockInAppFetcher = MockInAppFetcher() let mockUrlOpener = MockUrlOpener { url in XCTAssertEqual(url, TestInAppPayloadGenerator.getClickedUrl(index: 1)) - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) expectation1.fulfill() } @@ -235,7 +235,7 @@ class InAppTests: XCTestCase { mockInAppDisplayer.click(url: TestInAppPayloadGenerator.getClickedUrl(index: 1)) } - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer, urlOpener: mockUrlOpener @@ -363,7 +363,11 @@ class InAppTests: XCTestCase { urlOpener: mockUrlOpener ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getMessages() XCTAssertEqual(messages.count, 1) @@ -374,13 +378,14 @@ class InAppTests: XCTestCase { } wait(for: [expectation1, expectation2], timeout: testExpectationTimeout) + + XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) } - + func testShowInAppWithNoConsume() { - let expectation1 = expectation(description: "testShowInAppWithNoConsume") + let expectation1 = expectation(description: "testShowInAppWithConsume") let expectation2 = expectation(description: "url opened") - var internalApi: IterableAPIInternal! let mockInAppFetcher = MockInAppFetcher() let mockInAppDisplayer = MockInAppDisplayer() @@ -390,26 +395,27 @@ class InAppTests: XCTestCase { let mockUrlOpener = MockUrlOpener { url in XCTAssertEqual(url, TestInAppPayloadGenerator.getClickedUrl(index: 1)) - - let messages = internalApi.inAppManager.getMessages() - XCTAssertEqual(messages.count, 1) - XCTAssertEqual(messages[0].didProcessTrigger, true) expectation2.fulfill() } let config = IterableConfig() config.inAppDelegate = MockInAppDelegate(showInApp: .skip) - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer, urlOpener: mockUrlOpener ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getMessages() - // Now show the first message, but don't consume + XCTAssertEqual(messages.count, 1) + internalApi.inAppManager.show(message: messages[0], consume: false) { clickedUrl in XCTAssertEqual(clickedUrl, TestInAppPayloadGenerator.getClickedUrl(index: 1)) expectation1.fulfill() @@ -417,14 +423,14 @@ class InAppTests: XCTestCase { } wait(for: [expectation1, expectation2], timeout: testExpectationTimeout) + + XCTAssertEqual(internalApi.inAppManager.getMessages().count, 1) } - + func testShowInAppWithCustomAction() { let expectation1 = expectation(description: "testShowInAppWithCustomAction") let expectation2 = expectation(description: "custom action called") - let expectation3 = expectation(description: "count reduces") - - var internalApi: IterableAPIInternal! + let mockInAppFetcher = MockInAppFetcher() let mockInAppDisplayer = MockInAppDisplayer() @@ -436,10 +442,6 @@ class InAppTests: XCTestCase { mockCustomActionDelegate.callback = { customActionName, context in XCTAssertEqual(customActionName, TestInAppPayloadGenerator.getCustomActionName(index: 1)) XCTAssertEqual(context.source, .inApp) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) - expectation3.fulfill() - } expectation2.fulfill() } @@ -447,13 +449,17 @@ class InAppTests: XCTestCase { config.inAppDelegate = MockInAppDelegate(showInApp: .skip) config.customActionDelegate = mockCustomActionDelegate - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 1)).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getMessages() XCTAssertEqual(messages.count, 1, "expected 1 messages here") @@ -463,32 +469,26 @@ class InAppTests: XCTestCase { } } - wait(for: [expectation1, expectation2, expectation3], timeout: testExpectationTimeout) + wait(for: [expectation1, expectation2/*, expectation3*/], timeout: testExpectationTimeout) + XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) } func testShowInAppWithIterableCustomActionDelete() { - var internalApi: IterableAPIInternal! - let expectation1 = expectation(description: "correct number of messages") - let predicate = NSPredicate { (_, _) -> Bool in - internalApi.inAppManager.getMessages().count == 0 - } - let expectation2 = expectation(for: predicate, evaluatedWith: nil, handler: nil) - + let expectation1 = expectation(description: "message is shown") + let mockInAppFetcher = MockInAppFetcher() - let iterableDeleteUrl = "iterable://delete" let mockInAppDisplayer = MockInAppDisplayer() mockInAppDisplayer.onShow.onSuccess { _ in - let count = internalApi.inAppManager.getMessages().count - XCTAssertEqual(count, 1) mockInAppDisplayer.click(url: URL(string: iterableDeleteUrl)!) + expectation1.fulfill() } let config = IterableConfig() config.inAppDelegate = MockInAppDelegate(showInApp: .show) config.logDelegate = AllLogDelegate() - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer @@ -509,33 +509,29 @@ class InAppTests: XCTestCase { } """.toJsonDict() - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { _ in - expectation1.fulfill() - } + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload) wait(for: [expectation1], timeout: testExpectationTimeout) - wait(for: [expectation2], timeout: testExpectationTimeout) + + XCTAssertEqual(internalApi.inAppManager.getMessages().count, 0) } - + func testShowInAppWithIterableCustomActionDismiss() { - var internalApi: IterableAPIInternal! - let expectation1 = expectation(description: "messages fetched") - let expectation2 = expectation(description: "custom action dismiss called") - + let expectation1 = expectation(description: "message is shown") + let mockInAppFetcher = MockInAppFetcher() - - let iterableDismissUrl = "iterable://dismiss" + let iterableDeleteUrl = "iterable://dismiss" let mockInAppDisplayer = MockInAppDisplayer() mockInAppDisplayer.onShow.onSuccess { _ in - mockInAppDisplayer.click(url: URL(string: iterableDismissUrl)!) - XCTAssertEqual(internalApi.inAppManager.getMessages().count, 1) - expectation2.fulfill() + mockInAppDisplayer.click(url: URL(string: iterableDeleteUrl)!) + expectation1.fulfill() } let config = IterableConfig() config.inAppDelegate = MockInAppDelegate(showInApp: .show) + config.logDelegate = AllLogDelegate() - internalApi = IterableAPIInternal.initializeForTesting( + let internalApi = IterableAPIInternal.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher, inAppDisplayer: mockInAppDisplayer @@ -546,7 +542,7 @@ class InAppTests: XCTestCase { [ { "saveToInbox": true, - "content": {"contentType": "html", "inAppDisplaySettings": {"bottom": {"displayOption": "AutoExpand"}, "backgroundAlpha": 0.5, "left": {"percentage": 60}, "right": {"percentage": 60}, "top": {"displayOption": "AutoExpand"}}, "html": "Click Here"}, + "content": {"contentType": "html", "inAppDisplaySettings": {"bottom": {"displayOption": "AutoExpand"}, "backgroundAlpha": 0.5, "left": {"percentage": 60}, "right": {"percentage": 60}, "top": {"displayOption": "AutoExpand"}}, "html": "Click Here"}, "trigger": {"type": "immediate"}, "messageId": "message0", "campaignId": 1, @@ -555,15 +551,14 @@ class InAppTests: XCTestCase { ] } """.toJsonDict() - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { _ in - let messages = internalApi.inAppManager.getMessages() - XCTAssertEqual(messages.count, 1) - expectation1.fulfill() - } - wait(for: [expectation1, expectation2], timeout: testExpectationTimeout) + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload) + + wait(for: [expectation1], timeout: testExpectationTimeout) + + XCTAssertEqual(internalApi.inAppManager.getMessages().count, 1) } - + func testShowInAppWithCustomActionBackwardCompatibility() { let customActionScheme = "itbl" let customActionName = "my_custom_action" @@ -626,10 +621,18 @@ class InAppTests: XCTestCase { inAppFetcher: mockInAppFetcher ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 3)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 3)).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } XCTAssertEqual(internalApi.inAppManager.getMessages().count, 3) expectation1.fulfill() - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 2)).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, TestInAppPayloadGenerator.createPayloadWithUrl(numMessages: 2)).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } XCTAssertEqual(internalApi.inAppManager.getMessages().count, 2) expectation2.fulfill() } @@ -1048,7 +1051,11 @@ class InAppTests: XCTestCase { inAppPersister: InAppFilePersister() ) - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi1, TestInAppPayloadGenerator.createPayloadWithUrl(indices: [1, 3, 2])).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi1, TestInAppPayloadGenerator.createPayloadWithUrl(indices: [1, 3, 2])).onSuccess { [weak internalApi1] _ in + guard let internalApi1 = internalApi1 else { + XCTFail("Expected internalApi to be not nil") + return + } XCTAssertEqual(internalApi1.inAppManager.getMessages().count, 3) expectation1.fulfill() } @@ -1239,7 +1246,11 @@ class InAppTests: XCTestCase { } """.toJsonDict() - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payloadFromServer).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payloadFromServer).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getInboxMessages() XCTAssertEqual(messages.count, 2) @@ -1264,7 +1275,7 @@ class InAppTests: XCTestCase { """.toJsonDict() let mockNotificationCenter = MockNotificationCenter() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in expectation1.fulfill() } @@ -1364,7 +1375,11 @@ class InAppTests: XCTestCase { campaignId: 1, expiresAt: mockDateProvider.currentDate.addingTimeInterval(1.0 * 60.0), // one minute from now content: IterableHtmlInAppContent(edgeInsets: .zero, backgroundAlpha: 0.0, html: "")) - mockInAppFetcher.mockMessagesAvailableFromServer(internalApi: internalApi, messages: [message]).onSuccess { _ in + mockInAppFetcher.mockMessagesAvailableFromServer(internalApi: internalApi, messages: [message]).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } XCTAssertEqual(internalApi.inAppManager.getMessages().count, 1) mockDateProvider.currentDate = mockDateProvider.currentDate.addingTimeInterval(2.0 * 60) // two minutes from now @@ -1473,7 +1488,11 @@ class InAppTests: XCTestCase { } """.toJsonDict() - mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { _ in + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, payload).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("Expected internalApi to be not nil") + return + } let messages = internalApi.inAppManager.getMessages() XCTAssertEqual(messages.count, 1) expectation2.fulfill() diff --git a/tests/swift-sdk-swift-tests/InboxTests.swift b/tests/swift-sdk-swift-tests/InboxTests.swift index 19f723848..18cd06262 100644 --- a/tests/swift-sdk-swift-tests/InboxTests.swift +++ b/tests/swift-sdk-swift-tests/InboxTests.swift @@ -290,7 +290,7 @@ class InboxTests: XCTestCase { var callbackCount = 0 let mockInAppDelegate = MockInAppDelegate(showInApp: .skip) let mockNotificationCenter = MockNotificationCenter() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in let messages = internalAPI.inAppManager.getInboxMessages() if callbackCount == 0 { XCTAssertEqual(messages.count, 1) @@ -388,7 +388,7 @@ class InboxTests: XCTestCase { let mockInAppDelegate = MockInAppDelegate(showInApp: .skip) let mockNotificationCenter = MockNotificationCenter() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in DispatchQueue.main.async { let messages = internalAPI.inAppManager.getInboxMessages() XCTAssertEqual(messages.count, 1) @@ -437,7 +437,7 @@ class InboxTests: XCTestCase { var inboxCallbackCount = 0 let mockNotificationCenter = MockNotificationCenter() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in DispatchQueue.main.async { let messages = internalAPI.inAppManager.getInboxMessages() if inboxCallbackCount == 0 { @@ -634,7 +634,7 @@ class InboxTests: XCTestCase { XCTAssertEqual(internalAPI.inAppManager.getMessages().count, 1) expectation1.fulfill() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in expectation2.fulfill() } diff --git a/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift b/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift index 469b57bdb..73e969533 100644 --- a/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift +++ b/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift @@ -69,7 +69,7 @@ class IterableAPIResponseTests: XCTestCase { createApiClient(networkSession: MockNetworkSession(statusCode: 200, data: data)) .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() - XCTAssert(sendError.reason!.lowercased().contains("could not parse json")) + XCTAssert(sendError.reason!.lowercased().contains("could not convert data")) } wait(for: [xpectation], timeout: testExpectationTimeout) diff --git a/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift b/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift index 8ff96ebda..6acfc0540 100644 --- a/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift +++ b/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift @@ -8,10 +8,6 @@ import XCTest @testable import IterableSDK -class MockDateProvider: DateProviderProtocol { - var currentDate = Date() -} - class IterableNotificationResponseTests: XCTestCase { override func setUp() { super.setUp() diff --git a/tests/swift-sdk-swift-tests/IterableRequestTests.swift b/tests/swift-sdk-swift-tests/IterableRequestTests.swift new file mode 100644 index 000000000..aaa53fc5d --- /dev/null +++ b/tests/swift-sdk-swift-tests/IterableRequestTests.swift @@ -0,0 +1,71 @@ +// +// Created by Tapash Majumder on 7/29/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class IterableRequestTests: XCTestCase { + func testGetRequestSerialization() throws { + let path = "/a/b" + let args = ["var1": "val1", "var2": "val2"] + let request = IterableRequest.get(GetRequest(path: path, args: args)) + + let data = try JSONEncoder().encode(request) + let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) + if case let IterableRequest.get(request) = decoded { + XCTAssertEqual(request.path, path) + XCTAssertEqual(request.args, args) + } else { + XCTFail("Could not decode request properly") + } + } + + func testGetRequestSerializationWithNilArgs() throws { + let path = "/a/b" + let request = IterableRequest.get(GetRequest(path: path, args: nil)) + + let data = try JSONEncoder().encode(request) + let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) + if case let IterableRequest.get(request) = decoded { + XCTAssertEqual(request.path, path) + XCTAssertEqual(request.args, nil) + } else { + XCTFail("Could not decode request properly") + } + } + + func testPostRequestSerialization() throws { + let path = "/a/b" + let args = ["var1": "val1", "var2": "val2"] + let body: [AnyHashable: Any] = ["b1": "body1", "b2": true, "b3": 22] + let request = IterableRequest.post(PostRequest(path: path, args: args, body: body)) + + let data = try JSONEncoder().encode(request) + let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) + if case let IterableRequest.post(request) = decoded { + XCTAssertEqual(request.path, path) + XCTAssertEqual(request.args, args) + XCTAssertTrue(TestUtils.areEqual(dict1: request.body!, dict2: body)) + } else { + XCTFail("Could not decode request properly") + } + } + + func testPostRequestSerializationWithNilBody() throws { + let path = "/a/b" + let request = IterableRequest.post(PostRequest(path: path, args: nil, body: nil)) + + let data = try JSONEncoder().encode(request) + let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) + if case let IterableRequest.post(request) = decoded { + XCTAssertEqual(request.path, path) + XCTAssertNil(request.args) + XCTAssertNil(request.body) + } else { + XCTFail("Could not decode request properly") + } + } +} diff --git a/ui-tests-app/Info.plist b/ui-tests-app/Info.plist index 10f85c241..dffa2f8b6 100644 --- a/ui-tests-app/Info.plist +++ b/ui-tests-app/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - UITestApp + $(PRODUCT_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier