From d3840620044af7052f3aa97792872878774c8ccd Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 12:32:10 +0530 Subject: [PATCH 01/76] First version of Iterable core data stack with IterableTask. --- swift-sdk.xcodeproj/project.pbxproj | 45 ++++++ swift-sdk/Internal/CoreDataUtil.swift | 49 +++++++ .../IterableCoreDataPersistence.swift | 128 ++++++++++++++++++ .../IterableDataModel.xcdatamodel/contents | 20 +++ swift-sdk/Internal/IterablePersistence.swift | 36 +++++ swift-sdk/Internal/IterableTask.swift | 18 +++ swift-sdk/Internal/PersistenceHelper.swift | 31 +++++ tests/endpoint-tests/EndpointTests.swift | 4 +- tests/endpoint-tests/Environment.swift | 2 +- tests/swift-sdk-swift-tests/AuthTests.swift | 2 +- 10 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 swift-sdk/Internal/CoreDataUtil.swift create mode 100644 swift-sdk/Internal/IterableCoreDataPersistence.swift create mode 100644 swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents create mode 100644 swift-sdk/Internal/IterablePersistence.swift create mode 100644 swift-sdk/Internal/IterableTask.swift create mode 100644 swift-sdk/Internal/PersistenceHelper.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 61203f963..237d487a8 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -72,6 +72,10 @@ 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 */; }; AC64626B2140AACF0046E1BD /* IterableAPIResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */; }; @@ -115,6 +119,7 @@ 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 */; }; @@ -169,6 +174,7 @@ 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 */; }; 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 */; }; @@ -358,6 +364,10 @@ 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 = ""; }; AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPIResponseTests.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 = ""; }; @@ -400,6 +410,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; }; @@ -452,6 +463,7 @@ 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 = ""; }; 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 = ""; }; @@ -743,6 +755,19 @@ path = Resources; sourceTree = ""; }; + AC50865124C60133001DC132 /* Persistence */ = { + isa = PBXGroup; + children = ( + AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */, + AC50865524C603AC001DC132 /* IterablePersistence.swift */, + AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */, + AC50865724C60426001DC132 /* IterableTask.swift */, + AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */, + ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */, + ); + name = Persistence; + sourceTree = ""; + }; AC72A0AC20CF4C08004D7997 /* Util */ = { isa = PBXGroup; children = ( @@ -758,6 +783,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + AC50865124C60133001DC132 /* Persistence */, AC845105228DF5360052BB8F /* API Client */, AC0248062279132400495FB9 /* Dwifft */, AC426CC5211B5527002EDBE8 /* Fp */, @@ -1411,8 +1437,10 @@ AC3DD9C82142F3650046F886 /* ClassExtensions.swift in Sources */, AC219C50225FEDBD00B98631 /* IterableInboxCell.swift in Sources */, ACE6888D2228B86C00A95E5E /* InAppInternal.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 */, AC4B039622A8743F0043185B /* InAppManager+Functions.swift in Sources */, AC72A0D420CF4D19004D7997 /* IterableDeepLinkManager.swift in Sources */, @@ -1438,7 +1466,9 @@ 556FB1EA244FAF6A00EDF6BD /* InAppPresenter.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 */, @@ -1452,6 +1482,7 @@ AC845107228DF54E0052BB8F /* ApiClient.swift in Sources */, AC72A0D120CF4D0B004D7997 /* InAppHelper.swift in Sources */, AC8874AA22178BD80075B54B /* InAppContentParser.swift in Sources */, + AC50865624C603AC001DC132 /* IterablePersistence.swift in Sources */, AC776DA22118B86600C27C27 /* DeviceInfo.swift in Sources */, AC72A0CB20CF4CE2004D7997 /* IterableAPIInternal.swift in Sources */, AC72A0C720CF4CE2004D7997 /* CommerceItem.swift in Sources */, @@ -1463,6 +1494,7 @@ AC81918822713A110014955E /* Dwifft+UIKit.swift in Sources */, AC426226238C27DD00164121 /* IterableInboxCell+Layout.swift in Sources */, AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */, + AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2423,6 +2455,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/Internal/CoreDataUtil.swift b/swift-sdk/Internal/CoreDataUtil.swift new file mode 100644 index 000000000..499da5b26 --- /dev/null +++ b/swift-sdk/Internal/CoreDataUtil.swift @@ -0,0 +1,49 @@ +// +// 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) -> NSManagedObject { + NSEntityDescription.insertNewObject(forEntityName: entity, into: context) + } + + 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 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) + } + + 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/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift new file mode 100644 index 000000000..c4013584d --- /dev/null +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -0,0 +1,128 @@ +// +// Created by Tapash Majumder on 7/20/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import CoreData +import Foundation + +enum PersistenceConst { + static let dataModelFileName = "IterableDataModel" + enum EntityName { + static let task = "IterableTaskManagedObject" + } +} + +@available(iOS 10.0, *) +class PersistentContainer: NSPersistentContainer { + static let shared: PersistentContainer = { + let container = PersistentContainer(name: PersistenceConst.dataModelFileName) + 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()) { + self.dateProvider = dateProvider + } + + func newBackgroundContext() -> IterablePersistenceContext { + CoreDataPersistenceContext(managedObjectContext: PersistentContainer.shared.newBackgroundContext(), dateProvider: dateProvider) + } + + func mainQueueContext() -> IterablePersistenceContext { + CoreDataPersistenceContext(managedObjectContext: PersistentContainer.shared.viewContext, dateProvider: dateProvider) + } + + 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.created = 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.modified = dateProvider.currentDate + return PersistenceHelper.task(from: taskManagedObject) + } + + func delete(task: IterableTask) throws { + try deleteTask(withId: task.id) + } + + func createTask(id: String, processor: String) throws -> IterableTask { + guard let taskManagedObject = createTaskManagedObject() else { + throw IterableDBError.general("Could not create task managed object") + } + taskManagedObject.id = id + taskManagedObject.created = dateProvider.currentDate + taskManagedObject.processor = processor + return PersistenceHelper.task(from: taskManagedObject) + } + + 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 save() throws { + try managedObjectContext.save() + } + + private let managedObjectContext: NSManagedObjectContext + private let dateProvider: DateProviderProtocol + + private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? { + try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.EntityName.task, columnName: "id", columnValue: id) + } + + private func createTaskManagedObject() -> IterableTaskManagedObject? { + CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.EntityName.task) as? IterableTaskManagedObject + } +} diff --git a/swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents b/swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents new file mode 100644 index 000000000..588f6030f --- /dev/null +++ b/swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift new file mode 100644 index 000000000..961b57961 --- /dev/null +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -0,0 +1,36 @@ +// +// 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 localizedDescription: String { + switch self { + case let .general(description): + return description + } + } +} + +protocol IterablePersistenceContext { + func create(task: IterableTask) throws -> IterableTask + func update(task: IterableTask) throws -> IterableTask + func delete(task: IterableTask) throws + func createTask(id: String, processor: String) throws -> IterableTask + func findTask(withId id: String) throws -> IterableTask? + func deleteTask(withId id: String) throws + func save() throws +} + +protocol IterablePersistenceContextProvider { + func newBackgroundContext() -> IterablePersistenceContext + func mainQueueContext() -> IterablePersistenceContext +} diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift new file mode 100644 index 000000000..43e8d79ba --- /dev/null +++ b/swift-sdk/Internal/IterableTask.swift @@ -0,0 +1,18 @@ +// +// Created by Tapash Majumder on 7/20/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct IterableTask { + let id: String + let created: Date + var modified: Date? + let processor: String + var attempts: Int = 0 + var lastAttempt: Date? + var processing: Bool = false + var scheduleTime: Date? + var data: Data? +} diff --git a/swift-sdk/Internal/PersistenceHelper.swift b/swift-sdk/Internal/PersistenceHelper.swift new file mode 100644 index 000000000..c016f9008 --- /dev/null +++ b/swift-sdk/Internal/PersistenceHelper.swift @@ -0,0 +1,31 @@ +// +// 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!, + created: from.created!, + modified: from.modified, + processor: from.processor!, + attempts: Int(from.attempts), + lastAttempt: from.lastAttempt, + processing: from.processing, + scheduleTime: from.scheduleTime, + data: from.data) + } + + static func copy(from: IterableTask, to: IterableTaskManagedObject) { + to.created = from.created + to.modified = from.modified + to.processor = from.processor + to.attempts = Int64(from.attempts) + to.lastAttempt = from.lastAttempt + to.processing = from.processing + to.scheduleTime = from.scheduleTime + to.data = from.data + } +} diff --git a/tests/endpoint-tests/EndpointTests.swift b/tests/endpoint-tests/EndpointTests.swift index c922de831..bf3c414d2 100644 --- a/tests/endpoint-tests/EndpointTests.swift +++ b/tests/endpoint-tests/EndpointTests.swift @@ -25,7 +25,7 @@ class EndpointTests: XCTestCase { wait(for: [expectation1], timeout: 15) } - + func test02UpdateEmail() throws { let expectation1 = expectation(description: #function) let expectation2 = expectation(description: "New email is deleted") @@ -313,7 +313,7 @@ class EndpointTests: XCTestCase { clearAllInAppMessages(api: api) } - + private static let apiKey = Environment.apiKey! private static let pushCampaignId = Environment.pushCampaignId! private static let pushTemplateId = Environment.pushTemplateId! diff --git a/tests/endpoint-tests/Environment.swift b/tests/endpoint-tests/Environment.swift index adec2ea76..9ed20701f 100644 --- a/tests/endpoint-tests/Environment.swift +++ b/tests/endpoint-tests/Environment.swift @@ -47,7 +47,7 @@ struct Environment { } return Bool(strValue) ?? false } - + private static func getNSNumberFromEnv(key: Key) -> NSNumber? { if let strValue = getFromEnv(key: key), let intValue = Int(strValue) { return NSNumber(value: intValue) diff --git a/tests/swift-sdk-swift-tests/AuthTests.swift b/tests/swift-sdk-swift-tests/AuthTests.swift index e5c91d16b..5fd8584b1 100644 --- a/tests/swift-sdk-swift-tests/AuthTests.swift +++ b/tests/swift-sdk-swift-tests/AuthTests.swift @@ -157,7 +157,7 @@ class AuthTests: XCTestCase { wait(for: [condition1], timeout: testExpectationTimeout) } - + func testLogoutUser() { let localStorage = UserDefaultsLocalStorage(userDefaults: TestUtils.getTestUserDefaults()) From d72d183613cf930c7fea492c217192238b1cbb24 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 13:34:26 +0530 Subject: [PATCH 02/76] Fix for Cocoapods. --- Iterable-iOS-SDK.podspec | 2 +- swift-sdk.xcodeproj/project.pbxproj | 6 +++- .../Internal/IterableTaskManagedObject.swift | 29 +++++++++++++++++++ .../IterableDataModel.xcdatamodel/contents | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 swift-sdk/Internal/IterableTaskManagedObject.swift rename swift-sdk/{Internal => Resources}/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents (94%) diff --git a/Iterable-iOS-SDK.podspec b/Iterable-iOS-SDK.podspec index 55b8971d3..763804fbf 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/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 237d487a8..82eaaa242 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 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 */; }; + 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 */; }; @@ -464,6 +465,7 @@ 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 = ""; }; + 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 = ""; }; @@ -751,6 +753,7 @@ children = ( AC219C522260006600B98631 /* Assets.xcassets */, AC219C4F225FEDBD00B98631 /* SampleInboxCell.xib */, + AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */, ); path = Resources; sourceTree = ""; @@ -758,12 +761,12 @@ AC50865124C60133001DC132 /* Persistence */ = { isa = PBXGroup; children = ( - AC50865224C60172001DC132 /* IterableDataModel.xcdatamodeld */, AC50865524C603AC001DC132 /* IterablePersistence.swift */, AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */, AC50865724C60426001DC132 /* IterableTask.swift */, AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */, ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */, + ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */, ); name = Persistence; sourceTree = ""; @@ -1495,6 +1498,7 @@ AC426226238C27DD00164121 /* IterableInboxCell+Layout.swift in Sources */, AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */, AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */, + ACFD5AC824C8290E008E497A /* IterableTaskManagedObject.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/swift-sdk/Internal/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift new file mode 100644 index 000000000..7082c07d0 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -0,0 +1,29 @@ +// +// Created by Tapash Majumder on 7/22/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +/// This class mirrors what is automatically generated by Xcode. + +import Foundation +import CoreData + +@objc(IterableTaskManagedObject) +public class IterableTaskManagedObject: NSManagedObject { +} + +extension IterableTaskManagedObject { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: PersistenceConst.EntityName.task) + } + + @NSManaged public var attempts: Int64 + @NSManaged public var created: Date? + @NSManaged public var data: Data? + @NSManaged public var id: String? + @NSManaged public var lastAttempt: Date? + @NSManaged public var modified: Date? + @NSManaged public var processing: Bool + @NSManaged public var processor: String? + @NSManaged public var scheduleTime: Date? +} diff --git a/swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents similarity index 94% rename from swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents rename to swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents index 588f6030f..db0ce2892 100644 --- a/swift-sdk/Internal/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents +++ b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents @@ -1,6 +1,6 @@ - + From c83547d3ac10904271ff6d339ac8abab7540e1b4 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 20:57:21 +0530 Subject: [PATCH 03/76] Test for task create. --- swift-sdk.xcodeproj/project.pbxproj | 145 +++++++++++++++++- .../xcshareddata/xcschemes/swift-sdk.xcscheme | 24 +++ .../IterableCoreDataPersistence.swift | 8 +- swift-sdk/Internal/IterableTask.swift | 2 +- swift-sdk/Internal/PersistenceHelper.swift | 3 +- tests/offline-events-tests/Info.plist | 22 +++ .../offline-events-tests/TasksCRUDTests.swift | 41 +++++ 7 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 tests/offline-events-tests/Info.plist create mode 100644 tests/offline-events-tests/TasksCRUDTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 82eaaa242..6df055e71 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -175,6 +175,8 @@ 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 */; }; @@ -247,6 +249,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 */; @@ -466,6 +482,9 @@ ACF560E720E55A6B000AAC23 /* IterableActionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableActionContext.swift; sourceTree = ""; }; ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceHelper.swift; sourceTree = ""; }; ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableTaskManagedObject.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 = ""; }; 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 = ""; }; @@ -547,6 +566,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB524C8200C008E497A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ACFD5ABD24C8200C008E497A /* IterableSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF429224656BDF00FDF10D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -644,6 +671,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 = ../..; @@ -1011,10 +1039,20 @@ 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 */, + ); + path = "offline-events-tests"; + sourceTree = ""; + }; ACFF42A224656C6200FDF10D /* ui-tests-app */ = { isa = PBXGroup; children = ( @@ -1231,6 +1269,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" */; @@ -1256,7 +1313,7 @@ AC2263D620CF49B8009800EB /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1150; + LastSwiftUpdateCheck = 1160; LastUpgradeCheck = 0940; ORGANIZATIONNAME = Iterable; TargetAttributes = { @@ -1304,6 +1361,10 @@ }; }; }; + ACFD5AB724C8200C008E497A = { + CreatedOnToolsVersion = 11.6; + TestTargetID = ACF560D220E443BF000AAC23; + }; }; }; buildConfigurationList = AC2263D920CF49B8009800EB /* Build configuration list for PBXProject "swift-sdk" */; @@ -1332,6 +1393,7 @@ ACDA975823159C36004C412E /* inbox-ui-tests-app */, ACDA976B23159C39004C412E /* inbox-ui-tests-app-ui-tests */, AC28480624AA44C600C1FC7F /* endpoint-tests */, + ACFD5AB724C8200C008E497A /* offline-events-tests */, ); }; /* End PBXProject section */ @@ -1416,6 +1478,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB624C8200C008E497A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF429424656BDF00FDF10D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1637,6 +1706,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + ACFD5AB424C8200C008E497A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; ACFF428C24656BDF00FDF10D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1692,6 +1769,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 */; @@ -1943,7 +2030,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 = ( @@ -1964,7 +2051,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 = ( @@ -2295,6 +2382,49 @@ }; 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; + }; ACFF429C24656BDF00FDF10D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2430,6 +2560,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 = ( diff --git a/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme b/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme index 77d38b67d..498fa682c 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"> + + + + + + + + IterableTask { - guard let taskManagedObject = createTaskManagedObject() else { - throw IterableDBError.general("Could not create task managed object") - } - taskManagedObject.id = id - taskManagedObject.created = dateProvider.currentDate - taskManagedObject.processor = processor - return PersistenceHelper.task(from: taskManagedObject) + try create(task: IterableTask(id: id, processor: processor)) } func findTask(withId id: String) throws -> IterableTask? { diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index 43e8d79ba..a520af520 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -7,7 +7,7 @@ import Foundation struct IterableTask { let id: String - let created: Date + var created: Date? var modified: Date? let processor: String var attempts: Int = 0 diff --git a/swift-sdk/Internal/PersistenceHelper.swift b/swift-sdk/Internal/PersistenceHelper.swift index c016f9008..8746fd1de 100644 --- a/swift-sdk/Internal/PersistenceHelper.swift +++ b/swift-sdk/Internal/PersistenceHelper.swift @@ -8,7 +8,7 @@ import Foundation struct PersistenceHelper { static func task(from: IterableTaskManagedObject) -> IterableTask { IterableTask(id: from.id!, - created: from.created!, + created: from.created, modified: from.modified, processor: from.processor!, attempts: Int(from.attempts), @@ -19,6 +19,7 @@ struct PersistenceHelper { } static func copy(from: IterableTask, to: IterableTaskManagedObject) { + to.id = from.id to.created = from.created to.modified = from.modified to.processor = from.processor 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/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift new file mode 100644 index 000000000..c4cae9a39 --- /dev/null +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -0,0 +1,41 @@ +// +// Created by Tapash Majumder on 7/22/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class TasksCRUDTests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testCreate() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + let taskProcessor = "Processor1" + let task = try context.createTask(id: taskId, processor: taskProcessor) + try context.save() + XCTAssertEqual(task.id, taskId) + XCTAssertEqual(task.processor, taskProcessor) + + let newContext = persistenceProvider.mainQueueContext() + let found = try newContext.findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.processor, taskProcessor) + } + + func testUpdate() throws { + + } + + private lazy var persistenceProvider = { + CoreDataPersistenceContextProvider() + }() +} From 5016add362c0f9a778582a1b601fd7e62d413fa6 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 21:09:29 +0530 Subject: [PATCH 04/76] Update tests --- swift-sdk/Internal/IterablePersistence.swift | 9 +++++++ swift-sdk/Internal/IterableTask.swift | 16 ++++++++++++ .../offline-events-tests/TasksCRUDTests.swift | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index 961b57961..9f1fefb5b 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -21,12 +21,21 @@ extension IterableDBError: LocalizedError { } protocol IterablePersistenceContext { + @discardableResult func create(task: IterableTask) throws -> IterableTask + + @discardableResult func update(task: IterableTask) throws -> IterableTask + func delete(task: IterableTask) throws + + @discardableResult func createTask(id: String, processor: String) throws -> IterableTask + func findTask(withId id: String) throws -> IterableTask? + func deleteTask(withId id: String) throws + func save() throws } diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index a520af520..b9eac4fa1 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -15,4 +15,20 @@ struct IterableTask { var processing: Bool = false var scheduleTime: Date? var data: Data? + + func updated(attempts: Int? = nil, + lastAttempt: Date? = nil, + processing: Bool? = nil, + scheduleTime: Date? = nil, + data: Data? = nil) -> IterableTask { + IterableTask(id: self.id, + created: self.created, + modified: self.modified, + processor: self.processor, + attempts: attempts ?? self.attempts, + lastAttempt: lastAttempt ?? self.lastAttempt, + processing: processing ?? self.processing, + scheduleTime: scheduleTime ?? self.scheduleTime, + data: data ?? self.data) + } } diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index c4cae9a39..63ef56547 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -32,7 +32,32 @@ class TasksCRUDTests: XCTestCase { } func testUpdate() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + let taskProcessor = "Processor1" + let task = try context.createTask(id: taskId, processor: taskProcessor) + try context.save() + + let attempts = 2 + let lastAttempt = Date() + let processing = true + let scheduleTime = Date() + let data = Data(repeating: 1, count: 20) + let updatedTask = task.updated(attempts: attempts, lastAttempt: lastAttempt, processing: processing, scheduleTime: scheduleTime, data: data) + + 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.processor, taskProcessor) + XCTAssertNotNil(found.created) + XCTAssertNotNil(found.modified) + XCTAssertEqual(found.attempts, attempts) + XCTAssertEqual(found.lastAttempt, lastAttempt) + XCTAssertEqual(found.scheduleTime, scheduleTime) + XCTAssertEqual(found.data, data) } private lazy var persistenceProvider = { From 7a51d6cdb7c4032698c23fc2a5d1336af77a3023 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 21:19:42 +0530 Subject: [PATCH 05/76] Make IterableTask immutable. --- swift-sdk/Internal/IterableTask.swift | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index b9eac4fa1..3d5f6e5dd 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -7,14 +7,34 @@ import Foundation struct IterableTask { let id: String - var created: Date? - var modified: Date? + let created: Date? + let modified: Date? let processor: String - var attempts: Int = 0 - var lastAttempt: Date? - var processing: Bool = false - var scheduleTime: Date? - var data: Data? + let attempts: Int + let lastAttempt: Date? + let processing: Bool + let scheduleTime: Date? + let data: Data? + + init(id: String, + created: Date? = nil, + modified: Date? = nil, + processor: String, + attempts: Int = 0, + lastAttempt: Date? = nil, + processing: Bool = false, + scheduleTime: Date? = nil, + data: Data? = nil) { + self.id = id + self.created = created + self.modified = modified + self.processor = processor + self.attempts = attempts + self.lastAttempt = lastAttempt + self.processing = processing + self.scheduleTime = scheduleTime + self.data = data + } func updated(attempts: Int? = nil, lastAttempt: Date? = nil, From cac042744970092d4f3291a8b9485b7a5a80bcf9 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 22:06:37 +0530 Subject: [PATCH 06/76] Add delete tests --- .../offline-events-tests/TasksCRUDTests.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index 63ef56547..a9fad3cfa 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -60,6 +60,24 @@ class TasksCRUDTests: XCTestCase { XCTAssertEqual(found.data, data) } + func testDelete() throws { + let context = persistenceProvider.newBackgroundContext() + let taskId = IterableUtil.generateUUID() + let taskProcessor = "Processor1" + try context.createTask(id: taskId, processor: taskProcessor) + try context.save() + + let newContext = persistenceProvider.mainQueueContext() + let found = try newContext.findTask(withId: taskId)! + XCTAssertEqual(found.id, taskId) + XCTAssertEqual(found.processor, taskProcessor) + + try context.delete(task: found) + try context.save() + + XCTAssertNil(try newContext.findTask(withId: taskId)) + } + private lazy var persistenceProvider = { CoreDataPersistenceContextProvider() }() From 748cbb1f970fa539134127b4d2613e4f944af8bd Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 22 Jul 2020 22:40:00 +0530 Subject: [PATCH 07/76] Add findAll and deleteAll for tasks. --- swift-sdk/Internal/CoreDataUtil.swift | 9 +++-- .../IterableCoreDataPersistence.swift | 13 ++++++- swift-sdk/Internal/IterablePersistence.swift | 4 +++ .../offline-events-tests/TasksCRUDTests.swift | 34 +++++++++++++------ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/swift-sdk/Internal/CoreDataUtil.swift b/swift-sdk/Internal/CoreDataUtil.swift index 499da5b26..1ffeaa7db 100644 --- a/swift-sdk/Internal/CoreDataUtil.swift +++ b/swift-sdk/Internal/CoreDataUtil.swift @@ -9,8 +9,8 @@ import CoreData import Foundation struct CoreDataUtil { - static func create(context: NSManagedObjectContext, entity: String) -> NSManagedObject { - NSEntityDescription.insertNewObject(forEntityName: entity, into: context) + static func create(context: NSManagedObjectContext, entity: String) -> T? { + NSEntityDescription.insertNewObject(forEntityName: entity, into: context) as? T } static func findEntitiyByColumn(context: NSManagedObjectContext, @@ -20,6 +20,11 @@ struct CoreDataUtil { 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) diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index bc07f634b..3f03d3605 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -105,6 +105,17 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { managedObjectContext.delete(taskManagedObject) } + func findAllTasks() throws -> [IterableTask] { + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.EntityName.task) + + return taskManagedObjects.map(PersistenceHelper.task(from:)) + } + + func deleteAllTasks() throws { + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.EntityName.task) + taskManagedObjects.forEach { managedObjectContext.delete($0) } + } + func save() throws { try managedObjectContext.save() } @@ -117,6 +128,6 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { } private func createTaskManagedObject() -> IterableTaskManagedObject? { - CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.EntityName.task) as? IterableTaskManagedObject + return CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.EntityName.task) } } diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index 9f1fefb5b..c0522c113 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -35,6 +35,10 @@ protocol IterablePersistenceContext { func findTask(withId id: String) throws -> IterableTask? func deleteTask(withId id: String) throws + + func findAllTasks() throws -> [IterableTask] + + func deleteAllTasks() throws func save() throws } diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index a9fad3cfa..a7691c868 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -8,14 +8,6 @@ import XCTest @testable import IterableSDK class TasksCRUDTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - func testCreate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() @@ -78,7 +70,29 @@ class TasksCRUDTests: XCTestCase { XCTAssertNil(try newContext.findTask(withId: taskId)) } - private lazy var persistenceProvider = { - CoreDataPersistenceContextProvider() + func testFindAll() throws { + let context = persistenceProvider.newBackgroundContext() + try context.deleteAllTasks() + try context.save() + + let tasks = try context.findAllTasks() + XCTAssertEqual(tasks.count, 0) + + try context.createTask(id: IterableUtil.generateUUID(), processor: "First Processor") + try context.createTask(id: IterableUtil.generateUUID(), processor: "Second Processor") + try context.save() + + let newTasks = try context.findAllTasks() + XCTAssertEqual(newTasks.count, 2) + + try context.deleteAllTasks() + try context.save() + } + + private lazy var persistenceProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider() + try! provider.mainQueueContext().deleteAllTasks() + try! provider.mainQueueContext().save() + return provider }() } From 135a02e379e829c804209cc3f075d653f4d4c5fa Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 23 Jul 2020 11:21:00 +0530 Subject: [PATCH 08/76] Formatting with swiftformat --- .../Internal/IterableCoreDataPersistence.swift | 2 +- swift-sdk/Internal/IterablePersistence.swift | 8 ++++---- swift-sdk/Internal/IterableTask.swift | 8 ++++---- .../Internal/IterableTaskManagedObject.swift | 9 ++++----- tests/offline-events-tests/TasksCRUDTests.swift | 4 ++-- .../IterableAPIResponseTests.swift | 16 ++++++++-------- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 3f03d3605..659958011 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -128,6 +128,6 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { } private func createTaskManagedObject() -> IterableTaskManagedObject? { - return CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.EntityName.task) + CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.EntityName.task) } } diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index c0522c113..aa21db205 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -28,18 +28,18 @@ protocol IterablePersistenceContext { func update(task: IterableTask) throws -> IterableTask func delete(task: IterableTask) throws - + @discardableResult func createTask(id: String, processor: String) throws -> IterableTask - + func findTask(withId id: String) throws -> IterableTask? - + func deleteTask(withId id: String) throws func findAllTasks() throws -> [IterableTask] func deleteAllTasks() throws - + func save() throws } diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index 3d5f6e5dd..91942ce4d 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -41,10 +41,10 @@ struct IterableTask { processing: Bool? = nil, scheduleTime: Date? = nil, data: Data? = nil) -> IterableTask { - IterableTask(id: self.id, - created: self.created, - modified: self.modified, - processor: self.processor, + IterableTask(id: id, + created: created, + modified: modified, + processor: processor, attempts: attempts ?? self.attempts, lastAttempt: lastAttempt ?? self.lastAttempt, processing: processing ?? self.processing, diff --git a/swift-sdk/Internal/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift index 7082c07d0..3bcaaf821 100644 --- a/swift-sdk/Internal/IterableTaskManagedObject.swift +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -5,18 +5,17 @@ /// This class mirrors what is automatically generated by Xcode. -import Foundation import CoreData +import Foundation @objc(IterableTaskManagedObject) -public class IterableTaskManagedObject: NSManagedObject { -} +public class IterableTaskManagedObject: NSManagedObject {} extension IterableTaskManagedObject { @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: PersistenceConst.EntityName.task) + NSFetchRequest(entityName: PersistenceConst.EntityName.task) } - + @NSManaged public var attempts: Int64 @NSManaged public var created: Date? @NSManaged public var data: Data? diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index a7691c868..78a42e9d9 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -36,7 +36,7 @@ class TasksCRUDTests: XCTestCase { let scheduleTime = Date() let data = Data(repeating: 1, count: 20) let updatedTask = task.updated(attempts: attempts, lastAttempt: lastAttempt, processing: processing, scheduleTime: scheduleTime, data: data) - + try context.update(task: updatedTask) try context.save() @@ -84,7 +84,7 @@ class TasksCRUDTests: XCTestCase { let newTasks = try context.findAllTasks() XCTAssertEqual(newTasks.count, 2) - + try context.deleteAllTasks() try context.save() } diff --git a/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift b/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift index 25d306770..469b57bdb 100644 --- a/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift +++ b/tests/swift-sdk-swift-tests/IterableAPIResponseTests.swift @@ -56,7 +56,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("no data")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -70,7 +70,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("could not parse json")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -83,7 +83,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("invalid request")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -96,7 +96,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("test error")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -109,7 +109,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("invalid api key")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -122,7 +122,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("internal server error")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -135,7 +135,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("non-200 response")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } @@ -148,7 +148,7 @@ class IterableAPIResponseTests: XCTestCase { .send(iterableRequest: iterableRequest).onError { sendError in xpectation.fulfill() XCTAssert(sendError.reason!.lowercased().contains("nsurlerrordomain")) - } + } wait(for: [xpectation], timeout: testExpectationTimeout) } From a347a114564a1e937467e47b97a45ab6b2f80a72 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 29 Jul 2020 15:01:03 +0530 Subject: [PATCH 09/76] Make IterableRequest Codable. --- swift-sdk.xcodeproj/project.pbxproj | 18 +++- swift-sdk/Internal/IterableRequest.swift | 94 +++++++++++++++++++ swift-sdk/Internal/RequestCreator.swift | 18 ---- .../IterableRequestTests.swift | 71 ++++++++++++++ 4 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 swift-sdk/Internal/IterableRequest.swift create mode 100644 tests/swift-sdk-swift-tests/IterableRequestTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 6df055e71..a23571033 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -145,6 +145,8 @@ 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 */; }; 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 */; }; @@ -175,8 +177,8 @@ 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 */; }; + 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 */; }; @@ -449,6 +451,8 @@ 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 = ""; }; 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; }; @@ -481,10 +485,10 @@ 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 = ""; }; - 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 = ""; }; 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 = ""; }; @@ -874,6 +878,7 @@ AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */, 5585DF8E22A73390000A32B9 /* IterableInboxViewControllerTests.swift */, AC2C667F20D31B1F00D46CC9 /* IterableNotificationResponseTests.swift */, + ACC362B724D17005002C67BA /* IterableRequestTests.swift */, AC776DA3211A17C700C27C27 /* IterableRequestUtilTests.swift */, ACE34AB4213776CB00691224 /* LocalStorageTests.swift */, ACED4C00213F50B30055A497 /* LoggingTests.swift */, @@ -913,6 +918,7 @@ 55B9F15224B6625D00E8198A /* ApiClientProtocol.swift */, AC8E9267246284F800BEB68E /* DataFieldsHelper.swift */, AC84510822910A0C0052BB8F /* RequestCreator.swift */, + ACC362B524D16D91002C67BA /* IterableRequest.swift */, ); name = "API Client"; sourceTree = ""; @@ -1521,6 +1527,7 @@ AC72A0C920CF4CE2004D7997 /* IterableAction.swift in Sources */, AC72A0CA20CF4CE2004D7997 /* IterableActionRunner.swift in Sources */, ACD6116C2107D004003E7F6B /* NetworkHelper.swift in Sources */, + ACC362B624D16D91002C67BA /* IterableRequest.swift in Sources */, AC84510922910A0C0052BB8F /* RequestCreator.swift in Sources */, ACB8273F22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift in Sources */, AC81918A22713A400014955E /* AbstractDiffCalculator.swift in Sources */, @@ -1591,6 +1598,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 */, diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift new file mode 100644 index 000000000..06f9443ce --- /dev/null +++ b/swift-sdk/Internal/IterableRequest.swift @@ -0,0 +1,94 @@ +// +// 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.lowercased() { + case "get": + let request = try container.decode(GetRequest.self, forKey: .value) + self = .get(request) + case "post": + 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 .get(let request): + try container.encode("get", forKey: .type) + try container.encode(request, forKey: .value) + case .post(let request): + try container.encode("post", forKey: .type) + try container.encode(request, forKey: .value) + } + } +} + +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? = nil + 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/RequestCreator.swift b/swift-sdk/Internal/RequestCreator.swift index a054d0a5a..1a96249b7 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 diff --git a/tests/swift-sdk-swift-tests/IterableRequestTests.swift b/tests/swift-sdk-swift-tests/IterableRequestTests.swift new file mode 100644 index 000000000..a588a2c23 --- /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 IterableRequest.get(let 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 IterableRequest.get(let 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 IterableRequest.post(let 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 IterableRequest.post(let request) = decoded { + XCTAssertEqual(request.path, path) + XCTAssertNil(request.args) + XCTAssertNil(request.body) + } else { + XCTFail("Could not decode request properly") + } + } +} From ff146f4d3701810358d87bca3692953a4e7aa863 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 30 Jul 2020 22:44:25 +0530 Subject: [PATCH 10/76] Add support to persist iterable api call requests. Write processor class to take persisted value and make network call. --- swift-sdk.xcodeproj/project.pbxproj | 42 +++++++++++- swift-sdk/Internal/Auth.swift | 2 + .../Internal/IterableAPICallRequest.swift | 40 +++++++++++ .../IterableAPICallTaskProcessor.swift | 32 +++++++++ swift-sdk/Internal/IterableRequest.swift | 8 +-- swift-sdk/Internal/IterableTaskError.swift | 23 +++++++ .../Internal/IterableTaskProcessor.swift | 10 +++ swift-sdk/Internal/IterableTaskResult.swift | 25 +++++++ swift-sdk/IterableAPI.swift | 1 - .../TaskProcessorTests.swift | 66 +++++++++++++++++++ .../IterableRequestTests.swift | 10 +-- 11 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 swift-sdk/Internal/IterableAPICallRequest.swift create mode 100644 swift-sdk/Internal/IterableAPICallTaskProcessor.swift create mode 100644 swift-sdk/Internal/IterableTaskError.swift create mode 100644 swift-sdk/Internal/IterableTaskProcessor.swift create mode 100644 swift-sdk/Internal/IterableTaskResult.swift create mode 100644 tests/offline-events-tests/TaskProcessorTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index a23571033..681442bd7 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -147,6 +147,16 @@ 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 */; }; @@ -453,6 +463,12 @@ 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; }; @@ -795,7 +811,6 @@ children = ( AC50865524C603AC001DC132 /* IterablePersistence.swift */, AC50865924C60572001DC132 /* IterableCoreDataPersistence.swift */, - AC50865724C60426001DC132 /* IterableTask.swift */, AC8E7CA424C7555E0039605F /* CoreDataUtil.swift */, ACFD5AB224C8179D008E497A /* PersistenceHelper.swift */, ACFD5AC724C8290E008E497A /* IterableTaskManagedObject.swift */, @@ -818,6 +833,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + ACC362BB24D21153002C67BA /* Task Processing */, AC50865124C60133001DC132 /* Persistence */, AC845105228DF5360052BB8F /* API Client */, AC0248062279132400495FB9 /* Dwifft */, @@ -974,6 +990,19 @@ 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 */, + ); + name = "Task Processing"; + sourceTree = ""; + }; ACC87764215C20B50097E29B /* ui-tests */ = { isa = PBXGroup; children = ( @@ -1055,6 +1084,7 @@ children = ( ACFD5ABC24C8200C008E497A /* Info.plist */, ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, + ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1523,11 +1553,13 @@ 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 */, AC81918A22713A400014955E /* AbstractDiffCalculator.swift in Sources */, @@ -1562,12 +1594,15 @@ 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 */, AC72A0CB20CF4CE2004D7997 /* IterableAPIInternal.swift in Sources */, AC72A0C720CF4CE2004D7997 /* CommerceItem.swift in Sources */, AC31B040232AB42100BE25EB /* InboxSessionManager.swift in Sources */, ACE34AB321376B1000691224 /* UserDefaultsLocalStorage.swift in Sources */, AC8E9268246284F800BEB68E /* DataFieldsHelper.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 */, @@ -1719,6 +1754,11 @@ buildActionMask = 2147483647; files = ( ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, + ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, + ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */, + ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, + ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, + ACC362C524D2C190002C67BA /* TaskProcessorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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/IterableAPICallRequest.swift b/swift-sdk/Internal/IterableAPICallRequest.swift new file mode 100644 index 000000000..199698245 --- /dev/null +++ b/swift-sdk/Internal/IterableAPICallRequest.swift @@ -0,0 +1,40 @@ +// +// 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) + } + } + + 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..8b47c8ff4 --- /dev/null +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -0,0 +1,32 @@ +// +// 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 { + 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 { json in + result.resolve(with: .success(APICallTaskSuccess(json: json))) + } + .onError { networkError in + result.resolve(with: .failure(APICallTaskFailure(responseCode: nil, reason: networkError.reason, data: networkError.data))) + } + + return result + } +} diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift index 06f9443ce..b2163eecd 100644 --- a/swift-sdk/Internal/IterableRequest.swift +++ b/swift-sdk/Internal/IterableRequest.swift @@ -36,10 +36,10 @@ extension IterableRequest: Codable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case .get(let request): + case let .get(request): try container.encode("get", forKey: .type) try container.encode(request, forKey: .value) - case .post(let request): + case let .post(request): try container.encode("post", forKey: .type) try container.encode(request, forKey: .value) } @@ -83,12 +83,10 @@ extension PostRequest: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(path, forKey: .path) try container.encode(args, forKey: .args) - var bodyData: Data? = nil + 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/IterableTaskError.swift b/swift-sdk/Internal/IterableTaskError.swift new file mode 100644 index 000000000..e406637d5 --- /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 localizedDescription: String { + switch self { + case let .general(description): + return description ?? "general error" + } + } +} diff --git a/swift-sdk/Internal/IterableTaskProcessor.swift b/swift-sdk/Internal/IterableTaskProcessor.swift new file mode 100644 index 000000000..c55013dbf --- /dev/null +++ b/swift-sdk/Internal/IterableTaskProcessor.swift @@ -0,0 +1,10 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +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..61aafbb51 --- /dev/null +++ b/swift-sdk/Internal/IterableTaskResult.swift @@ -0,0 +1,25 @@ +// +// Created by Tapash Majumder on 7/30/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +enum IterableTaskResult { + case success(TaskSuccess) + case failure(TaskFailure) +} + +protocol TaskSuccess {} + +struct APICallTaskSuccess: TaskSuccess { + let json: SendRequestValue +} + +protocol TaskFailure {} + +struct APICallTaskFailure: TaskFailure { + let responseCode: Int? + let reason: String? + let data: Data? +} diff --git a/swift-sdk/IterableAPI.swift b/swift-sdk/IterableAPI.swift index cb49fb390..03b4096e8 100644 --- a/swift-sdk/IterableAPI.swift +++ b/swift-sdk/IterableAPI.swift @@ -68,7 +68,6 @@ import UIKit initialize(apiKey: apiKey, launchOptions: nil, config: config) } - /// An SDK initializer taking in the Iterable Mobile API key to be utilized and the /// `launchOptions` passed on from the app delegate, using default SDK settings /// - Parameters: diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift new file mode 100644 index 000000000..7b2ef4048 --- /dev/null +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -0,0 +1,66 @@ +// +// 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 Result.success(let 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) + + let taskId = IterableUtil.generateUUID() + let taskProcessor = "APICallTaskProcessor" + try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + try persistenceProvider.mainQueueContext().save() + + let found = try persistenceProvider.mainQueueContext().findTask(withId: taskId)! + + 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) + } + + private lazy var persistenceProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider() + try! provider.mainQueueContext().deleteAllTasks() + try! provider.mainQueueContext().save() + return provider + }() +} diff --git a/tests/swift-sdk-swift-tests/IterableRequestTests.swift b/tests/swift-sdk-swift-tests/IterableRequestTests.swift index a588a2c23..aaa53fc5d 100644 --- a/tests/swift-sdk-swift-tests/IterableRequestTests.swift +++ b/tests/swift-sdk-swift-tests/IterableRequestTests.swift @@ -15,7 +15,7 @@ class IterableRequestTests: XCTestCase { let data = try JSONEncoder().encode(request) let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) - if case IterableRequest.get(let request) = decoded { + if case let IterableRequest.get(request) = decoded { XCTAssertEqual(request.path, path) XCTAssertEqual(request.args, args) } else { @@ -29,14 +29,14 @@ class IterableRequestTests: XCTestCase { let data = try JSONEncoder().encode(request) let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) - if case IterableRequest.get(let request) = decoded { + 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"] @@ -45,7 +45,7 @@ class IterableRequestTests: XCTestCase { let data = try JSONEncoder().encode(request) let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) - if case IterableRequest.post(let request) = decoded { + if case let IterableRequest.post(request) = decoded { XCTAssertEqual(request.path, path) XCTAssertEqual(request.args, args) XCTAssertTrue(TestUtils.areEqual(dict1: request.body!, dict2: body)) @@ -60,7 +60,7 @@ class IterableRequestTests: XCTestCase { let data = try JSONEncoder().encode(request) let decoded = try JSONDecoder().decode(IterableRequest.self, from: data) - if case IterableRequest.post(let request) = decoded { + if case let IterableRequest.post(request) = decoded { XCTAssertEqual(request.path, path) XCTAssertNil(request.args) XCTAssertNil(request.body) From 907315b3cc6e875b7f3502871d8a89e187a1e73b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 3 Aug 2020 22:16:57 +0530 Subject: [PATCH 11/76] Comments --- tests/offline-events-tests/TaskProcessorTests.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift index 7b2ef4048..1b977226d 100644 --- a/tests/offline-events-tests/TaskProcessorTests.swift +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -19,16 +19,15 @@ class TaskProcessorTests: XCTestCase { 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 Result.success(let trackEventRequest) = requestCreator.createTrackEventRequest(eventName, dataFields: dataFields) else { + 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, @@ -36,13 +35,16 @@ class TaskProcessorTests: XCTestCase { iterableRequest: trackEventRequest) let data = try JSONEncoder().encode(apiCallRequest) + // persist data let taskId = IterableUtil.generateUUID() let taskProcessor = "APICallTaskProcessor" try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) 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] @@ -50,13 +52,13 @@ class TaskProcessorTests: XCTestCase { 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) } - + private lazy var persistenceProvider: IterablePersistenceContextProvider = { let provider = CoreDataPersistenceContextProvider() try! provider.mainQueueContext().deleteAllTasks() From f63ccc3c5f45f6335cbe812b948c3d024a62d384 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 11:07:27 +0530 Subject: [PATCH 12/76] Do not hardcode "get" and "post" --- swift-sdk/Internal/IterableRequest.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift index 06f9443ce..cb4584765 100644 --- a/swift-sdk/Internal/IterableRequest.swift +++ b/swift-sdk/Internal/IterableRequest.swift @@ -21,11 +21,11 @@ extension IterableRequest: Codable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) - switch type.lowercased() { - case "get": + switch type { + case IterableRequest.requestTypeGet: let request = try container.decode(GetRequest.self, forKey: .value) self = .get(request) - case "post": + case IterableRequest.requestTypePost: let request = try container.decode(PostRequest.self, forKey: .value) self = .post(request) default: @@ -37,13 +37,16 @@ extension IterableRequest: Codable { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .get(let request): - try container.encode("get", forKey: .type) + try container.encode(IterableRequest.requestTypeGet, forKey: .type) try container.encode(request, forKey: .value) case .post(let request): - try container.encode("post", forKey: .type) + 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 { From 44a70bd5e55bb3d3651253ef4c84821787862802 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 20:05:54 +0530 Subject: [PATCH 13/76] Move registerToken to IterableRequestProcessor --- swift-sdk.xcodeproj/project.pbxproj | 12 +++ swift-sdk/Internal/IterableAPIInternal.swift | 78 ++++---------- swift-sdk/Internal/IterableRequest.swift | 6 +- .../Internal/IterableRequestProcessor.swift | 101 ++++++++++++++++++ swift-sdk/Internal/NetworkHelper.swift | 4 + swift-sdk/Internal/Promise.swift | 14 +++ 6 files changed, 157 insertions(+), 58 deletions(-) create mode 100644 swift-sdk/Internal/IterableRequestProcessor.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 681442bd7..69afd73aa 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 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 */; }; + AC5E888924E1B7CE00752321 /* IterableRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */; }; AC64626B2140AACF0046E1BD /* IterableAPIResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */; }; @@ -397,6 +398,7 @@ 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 = ""; }; + AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequestProcessor.swift; sourceTree = ""; }; AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPIResponseTests.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 = ""; }; @@ -818,6 +820,14 @@ name = Persistence; sourceTree = ""; }; + AC5E888724E1B7AD00752321 /* Event Processing */ = { + isa = PBXGroup; + children = ( + AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */, + ); + name = "Event Processing"; + sourceTree = ""; + }; AC72A0AC20CF4C08004D7997 /* Util */ = { isa = PBXGroup; children = ( @@ -833,6 +843,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + AC5E888724E1B7AD00752321 /* Event Processing */, ACC362BB24D21153002C67BA /* Task Processing */, AC50865124C60133001DC132 /* Persistence */, AC845105228DF5360052BB8F /* API Client */, @@ -1601,6 +1612,7 @@ AC31B040232AB42100BE25EB /* InboxSessionManager.swift in Sources */, ACE34AB321376B1000691224 /* UserDefaultsLocalStorage.swift in Sources */, AC8E9268246284F800BEB68E /* DataFieldsHelper.swift in Sources */, + AC5E888924E1B7CE00752321 /* IterableRequestProcessor.swift in Sources */, ACC362C324D21332002C67BA /* IterableTaskError.swift in Sources */, ACC362C124D21272002C67BA /* IterableTaskResult.swift in Sources */, AC2C667E20D3111900D46CC9 /* DateProvider.swift in Sources */, diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 23619bcde..f22e17984 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -151,32 +151,29 @@ 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) } - // check notificationsEnabled then call register with enabled/not-not enabled - notificationStateProvider.notificationsEnabled.onSuccess { enabled in - self.register(token: token, - appName: appName, - pushServicePlatform: self.config.pushPlatform, - notificationsEnabled: enabled, - onSuccess: onSuccess, - onFailure: onFailure) - }.onError { _ in - self.register(token: token, - appName: appName, - pushServicePlatform: self.config.pushPlatform, - notificationsEnabled: false, - onSuccess: onSuccess, - onFailure: onFailure) - } + let registerTokenInfo = IterableRequestProcessor.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"), @@ -390,6 +387,10 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deviceMetadata: deviceMetadata) }() + lazy var requestProcessor: IterableRequestProcessor = { + IterableRequestProcessor(apiClient: self.apiClient) + }() + private var deviceAttributes = [String: String]() private var pushIntegrationName: String? { @@ -447,17 +448,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 storeAuthData() { localStorage.email = _email localStorage.userId = _userId @@ -470,28 +460,6 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { authToken = localStorage.authToken } - @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, - 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, diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift index 2adbccd8c..b2de3c2a5 100644 --- a/swift-sdk/Internal/IterableRequest.swift +++ b/swift-sdk/Internal/IterableRequest.swift @@ -36,15 +36,15 @@ extension IterableRequest: Codable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case .get(let request): + case let .get(request): try container.encode(IterableRequest.requestTypeGet, forKey: .type) try container.encode(request, forKey: .value) - case .post(let request): + 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" } diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift new file mode 100644 index 000000000..cbf754d1f --- /dev/null +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -0,0 +1,101 @@ +// +// Created by Tapash Majumder on 8/10/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +struct IterableRequestProcessor { + let apiClient: ApiClientProtocol! + + struct RegisterTokenInfo { + let hexToken: String + let appName: String + let pushServicePlatform: PushServicePlatform + let apnsType: APNSType + let deviceId: String + let deviceAttributes: [String: String] + let sdkVersion: String? + } + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("registerToken"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("registerToken")) -> Future { + // check notificationsEnabled then call register with enabled/not-enabled + notificationStateProvider.notificationsEnabled + .mapFailure(SendRequestError.from(error:)) + .replaceError(with: false) + .flatMap { enabled in + self.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: enabled, + onSuccess: onSuccess, + onFailure: onFailure) + } + } + + @discardableResult + private func register(registerTokenInfo: RegisterTokenInfo, + notificationsEnabled: Bool, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + let pushServicePlatformString = IterableRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) + + return IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, + appName: registerTokenInfo.appName, + deviceId: registerTokenInfo.deviceId, + sdkVersion: registerTokenInfo.sdkVersion, + deviceAttributes: registerTokenInfo.deviceAttributes, + pushServicePlatform: pushServicePlatformString, + notificationsEnabled: notificationsEnabled)) + } + + 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 + } + } + + @discardableResult + private static func call(successHandler onSuccess: OnSuccessHandler? = nil, + andFailureHandler onFailure: OnFailureHandler? = nil, + forResult result: Future) -> Future { + result.onSuccess { json in + onSuccess?(json) + }.onError { error in + onFailure?(error.reason, error.data) + } + return result + } + + 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) + } + } +} diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 87e520027..f880c4813 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -19,6 +19,10 @@ struct SendRequestError: Error { static func createErroredFuture(reason: String? = nil) -> Future { Promise(error: SendRequestError(reason: reason)) } + + static func from(error: Error) -> SendRequestError { + SendRequestError(reason: error.localizedDescription) + } } extension SendRequestError: LocalizedError { diff --git a/swift-sdk/Internal/Promise.swift b/swift-sdk/Internal/Promise.swift index 837c440d2..dde72fb56 100644 --- a/swift-sdk/Internal/Promise.swift +++ b/swift-sdk/Internal/Promise.swift @@ -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 From a57eb056fe576d13f3ed59c894e22dc09bd4331b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 20:10:49 +0530 Subject: [PATCH 14/76] Set hexToken --- swift-sdk/Internal/IterableAPIInternal.swift | 3 ++- .../Internal/IterableRequestProcessor.swift | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index f22e17984..b6d3e1e49 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -162,7 +162,8 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { onFailure?(errorMessage, nil) return SendRequestError.createErroredFuture(reason: errorMessage) } - + + hexToken = token.hexString() let registerTokenInfo = IterableRequestProcessor.RegisterTokenInfo(hexToken: token.hexString(), appName: appName, pushServicePlatform: config.pushPlatform, diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index cbf754d1f..3b670a5cb 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -53,6 +53,26 @@ struct IterableRequestProcessor { notificationsEnabled: notificationsEnabled)) } +// 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, +// forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) +// } + private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { switch pushServicePlatform { case .production: From a244468d3926ec7705489355a90c5adcd8db4933 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 20:38:46 +0530 Subject: [PATCH 15/76] Move disable device calls to Request Processor --- swift-sdk/Internal/IterableAPIInternal.swift | 52 +++++++++---------- .../Internal/IterableRequestProcessor.swift | 44 +++++++++------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index b6d3e1e49..6eb778fee 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -177,14 +177,32 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { 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) } func updateUser(_ dataFields: [AnyHashable: Any], @@ -474,26 +492,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, - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) - } - @discardableResult private static func call(successHandler onSuccess: OnSuccessHandler? = nil, andFailureHandler onFailure: OnFailureHandler? = nil, diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 3b670a5cb..0abf7bbbc 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -34,6 +34,20 @@ struct IterableRequestProcessor { onFailure: onFailure) } } + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("disableDeviceForCurrentUser"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("disableDeviceForCurrentUser")) -> Future { + return disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + } + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("disableDeviceForAllUsers"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("disableDeviceForAllUsers")) -> Future { + return disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + } @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, @@ -52,26 +66,16 @@ struct IterableRequestProcessor { pushServicePlatform: pushServicePlatformString, notificationsEnabled: notificationsEnabled)) } - -// 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, -// forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) -// } + + @discardableResult + private func disableDevice(forAllUsers allUsers: Bool, + hexToken: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + return IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + } private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { switch pushServicePlatform { From 33abcb40c83cbf947b5df5aa3871bf736540e004 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 20:48:08 +0530 Subject: [PATCH 16/76] Wait for 60 seconds for endpoint tests --- tests/endpoint-tests/EndpointTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 { From 25ce0cac525493cd2bd999dff6539623ce94252d Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 11 Aug 2020 21:12:32 +0530 Subject: [PATCH 17/76] Move updateUser and updateEmail to Iterable request processor. --- swift-sdk/Internal/IterableAPIInternal.swift | 20 +++++++---------- .../Internal/IterableRequestProcessor.swift | 22 ++++++++++++++++++- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 6eb778fee..dea8d22c3 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -205,28 +205,24 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { 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, - 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, withToken token: String? = nil, - onSuccess: OnSuccessHandler? = IterableAPIInternal.defaultOnSuccess("updateEmail"), - onFailure: OnFailureHandler? = IterableAPIInternal.defaultOnFailure("updateEmail")) { - apiClient.updateEmail(newEmail: newEmail).onSuccess { json in + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.updateEmail(newEmail, withToken: token, onSuccess: onSuccess, onFailure: onFailure).onSuccess { _ in // only change email if one is being used if self.email != nil { self.setEmail(newEmail, withToken: token) } - - onSuccess?(json) - }.onError { error in - onFailure?(error.reason, error.data) } } diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 0abf7bbbc..78bdf884c 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -48,7 +48,27 @@ struct IterableRequestProcessor { onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("disableDeviceForAllUsers")) -> Future { return disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) } - + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateUser"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateUser")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + } + + @discardableResult + func updateEmail(_ newEmail: String, + withToken token: String? = nil, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateEmail"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateEmail")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.updateEmail(newEmail: newEmail)) + } + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From b9264e2863fed74bc0284d83cf05ff64ae9a0516 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 11:40:23 +0530 Subject: [PATCH 18/76] Move trackPurchase and trackPushOpen to IterableRequestProcessor --- swift-sdk/Internal/IterableAPIInternal.swift | 49 ++++++++++--------- .../IterableAppIntegrationInternal.swift | 8 +-- .../Internal/IterableRequestProcessor.swift | 28 +++++++++++ tests/common/CommonMocks.swift | 16 ++++-- 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index dea8d22c3..7225f23ad 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -226,49 +226,50 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { } } + @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, - 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, - 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) } func track(_ eventName: String, diff --git a/swift-sdk/Internal/IterableAppIntegrationInternal.swift b/swift-sdk/Internal/IterableAppIntegrationInternal.swift index b99021116..faef63b27 100644 --- a/swift-sdk/Internal/IterableAppIntegrationInternal.swift +++ b/swift-sdk/Internal/IterableAppIntegrationInternal.swift @@ -76,21 +76,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 { diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 78bdf884c..937a651fe 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -68,6 +68,34 @@ struct IterableRequestProcessor { andFailureHandler: onFailure, forResult: apiClient.updateEmail(newEmail: newEmail)) } + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]? = nil, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackPurchase"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackPurchase")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + 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? = IterableRequestProcessor.defaultOnSuccess("trackPushOpen"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackPushOpen")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.track(pushOpen: campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields)) + } @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index f64e57c2e..b1e15a8ae 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -105,18 +105,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 +130,8 @@ public class MockPushTracker: NSObject, PushTrackerProtocol { self.dataFields = dataFields self.onSuccess = onSuccess self.onFailure = onFailure + + return Promise(value: [:]) } } From a63af5a245bd0275ead890544d36eb95d432341b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 12:00:30 +0530 Subject: [PATCH 19/76] Move trackEvent and updateSubscriptions to IterableRequestProcessor. --- swift-sdk.xcodeproj/project.pbxproj | 6 ++-- swift-sdk/Internal/IterableAPIInternal.swift | 29 ++++++++-------- .../Internal/IterableRequestProcessor.swift | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 69afd73aa..0650ef8c0 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -820,12 +820,12 @@ name = Persistence; sourceTree = ""; }; - AC5E888724E1B7AD00752321 /* Event Processing */ = { + AC5E888724E1B7AD00752321 /* Request Processing */ = { isa = PBXGroup; children = ( AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */, ); - name = "Event Processing"; + name = "Request Processing"; sourceTree = ""; }; AC72A0AC20CF4C08004D7997 /* Util */ = { @@ -843,7 +843,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( - AC5E888724E1B7AD00752321 /* Event Processing */, + AC5E888724E1B7AD00752321 /* Request Processing */, ACC362BB24D21153002C67BA /* Task Processing */, AC50865124C60133001DC132 /* Persistence */, AC845105228DF5360052BB8F /* API Client */, diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 7225f23ad..fdb6086cc 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -272,31 +272,30 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { 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, - 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, - forResult: apiClient.updateSubscriptions(emailListIds, - unsubscribedChannelIds: unsubscribedChannelIds, - unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, - subscribedMessageTypeIds: subscribedMessageTypeIds, - campaignId: campaignId, - templateId: templateId)) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let updateSubscriptionsInfo = IterableRequestProcessor.UpdateSubscriptionsInfo(emailListIds: emailListIds, + unsubscribedChannelIds: unsubscribedChannelIds, + unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, + subscribedMessageTypeIds: subscribedMessageTypeIds, + campaignId: campaignId, + templateId: templateId) + return requestProcessor.updateSubscriptions(info: updateSubscriptionsInfo, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 937a651fe..c3a1aabca 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -5,6 +5,7 @@ import Foundation +/// `IterableAPIinternal` will delegate all network related calls to this struct. struct IterableRequestProcessor { let apiClient: ApiClientProtocol! @@ -18,6 +19,15 @@ struct IterableRequestProcessor { let sdkVersion: String? } + struct UpdateSubscriptionsInfo { + let emailListIds: [NSNumber]? + let unsubscribedChannelIds: [NSNumber]? + let unsubscribedMessageTypeIds: [NSNumber]? + let subscribedMessageTypeIds: [NSNumber]? + let campaignId: NSNumber? + let templateId: NSNumber? + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, @@ -97,6 +107,30 @@ struct IterableRequestProcessor { dataFields: dataFields)) } + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]? = nil, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackEvent"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackEvent")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.track(event: event, dataFields: dataFields)) + } + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateSubscriptions"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateSubscriptions")) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: apiClient.updateSubscriptions(info.emailListIds, + unsubscribedChannelIds: info.unsubscribedChannelIds, + unsubscribedMessageTypeIds: info.unsubscribedMessageTypeIds, + subscribedMessageTypeIds: info.subscribedMessageTypeIds, + campaignId: info.campaignId, + templateId: info.templateId)) + } + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From 19cddeaa63806115290ceff95c6ffb3be77ed8d2 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 12:51:31 +0530 Subject: [PATCH 20/76] Move trackInAppOpen to IterableRequestProcessor --- swift-sdk/Internal/IterableAPIInternal.swift | 13 +++++++------ swift-sdk/Internal/IterableRequestProcessor.swift | 13 +++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index fdb6086cc..a4f65720f 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -302,12 +302,13 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { 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, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppOpen(message, + location: location, + inboxSessionId: inboxSessionId, + onSuccess: onSuccess, + onFailure: onFailure) } @discardableResult diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index c3a1aabca..3ad20ccb4 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -131,6 +131,19 @@ struct IterableRequestProcessor { templateId: info.templateId)) } + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String? = nil, + onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackInAppOpen"), + onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackInAppOpen")) -> Future { + let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) + return IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + forResult: result) + } + + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From f4acd09cad7813c8b25b8b0f96b7c0cd8c3a718c Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 13:10:47 +0530 Subject: [PATCH 21/76] Refactor defaultOnSuccess and defaultOnFailure --- .../Internal/IterableRequestProcessor.swift | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 3ad20ccb4..2b0b5936a 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -31,8 +31,8 @@ struct IterableRequestProcessor { @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("registerToken"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("registerToken")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { // check notificationsEnabled then call register with enabled/not-enabled notificationStateProvider.notificationsEnabled .mapFailure(SendRequestError.from(error:)) @@ -47,46 +47,49 @@ struct IterableRequestProcessor { @discardableResult func disableDeviceForCurrentUser(hexToken: String, - withOnSuccess onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("disableDeviceForCurrentUser"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("disableDeviceForCurrentUser")) -> Future { + withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { return disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult func disableDeviceForAllUsers(hexToken: String, - withOnSuccess onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("disableDeviceForAllUsers"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("disableDeviceForAllUsers")) -> Future { + withOnSuccess onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { return disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateUser"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateUser")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "updateUser", forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) } @discardableResult func updateEmail(_ newEmail: String, withToken token: String? = nil, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateEmail"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateEmail")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - forResult: apiClient.updateEmail(newEmail: newEmail)) + andFailureHandler: onFailure, + withIdentifier: "updateEmail", + forResult: apiClient.updateEmail(newEmail: newEmail)) } @discardableResult func trackPurchase(_ total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackPurchase"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackPurchase")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "trackPurchase", forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) } @@ -96,10 +99,11 @@ struct IterableRequestProcessor { messageId: String?, appAlreadyRunning: Bool, dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackPushOpen"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackPushOpen")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "trackPushOpen", forResult: apiClient.track(pushOpen: campaignId, templateId: templateId, messageId: messageId, @@ -110,19 +114,21 @@ struct IterableRequestProcessor { @discardableResult func track(event: String, dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackEvent"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackEvent")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "trackEvent", forResult: apiClient.track(event: event, dataFields: dataFields)) } @discardableResult func updateSubscriptions(info: UpdateSubscriptionsInfo, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("updateSubscriptions"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("updateSubscriptions")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "updateSubscriptions", forResult: apiClient.updateSubscriptions(info.emailListIds, unsubscribedChannelIds: info.unsubscribedChannelIds, unsubscribedMessageTypeIds: info.unsubscribedMessageTypeIds, @@ -135,11 +141,12 @@ struct IterableRequestProcessor { func trackInAppOpen(_ message: IterableInAppMessage, location: InAppLocation, inboxSessionId: String? = nil, - onSuccess: OnSuccessHandler? = IterableRequestProcessor.defaultOnSuccess("trackInAppOpen"), - onFailure: OnFailureHandler? = IterableRequestProcessor.defaultOnFailure("trackInAppOpen")) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) return IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", forResult: result) } @@ -147,12 +154,13 @@ struct IterableRequestProcessor { @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, - onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { let pushServicePlatformString = IterableRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) return IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, + withIdentifier: "registerToken", forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, appName: registerTokenInfo.appName, deviceId: registerTokenInfo.deviceId, @@ -165,11 +173,12 @@ struct IterableRequestProcessor { @discardableResult private func disableDevice(forAllUsers allUsers: Bool, hexToken: String, - onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) } private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { @@ -186,11 +195,20 @@ struct IterableRequestProcessor { @discardableResult private static func call(successHandler onSuccess: OnSuccessHandler? = nil, andFailureHandler onFailure: OnFailureHandler? = nil, + withIdentifier identifier: String, forResult result: Future) -> Future { result.onSuccess { json in - onSuccess?(json) + if let onSuccess = onSuccess { + onSuccess(json) + } else { + defaultOnSuccess(identifier)(json) + } }.onError { error in - onFailure?(error.reason, error.data) + if let onFailure = onFailure { + onFailure(error.reason, error.data) + } else { + defaultOnFailure(identifier)(error.reason, error.data) + } } return result } From 6fde5a1a4a229a47d2e90903b8558f8aac47b0e3 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 13:49:47 +0530 Subject: [PATCH 22/76] Move trackInAppClick, trackInAppClose, trackInboxSession to IterableRequestProcessor. --- swift-sdk/Internal/IterableAPIInternal.swift | 47 +++---- .../Internal/IterableRequestProcessor.swift | 125 ++++++++++++------ 2 files changed, 106 insertions(+), 66 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index a4f65720f..d08a4e902 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -151,7 +151,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { } // MARK: - API Request Calls - + @discardableResult func register(token: Data, onSuccess: OnSuccessHandler? = nil, @@ -162,7 +162,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { onFailure?(errorMessage, nil) return SendRequestError.createErroredFuture(reason: errorMessage) } - + hexToken = token.hexString() let registerTokenInfo = IterableRequestProcessor.RegisterTokenInfo(hexToken: token.hexString(), appName: appName, @@ -193,7 +193,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { return requestProcessor.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure) } - + @discardableResult func disableDeviceForAllUsers(withOnSuccess onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { @@ -316,13 +316,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, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.trackInAppClick(message, location: location, + inboxSessionId: inboxSessionId, + clickedUrl: clickedUrl, + onSuccess: onSuccess, + onFailure: onFailure) } @discardableResult @@ -331,25 +331,22 @@ 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, - 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, - forResult: result) + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + requestProcessor.track(inboxSession: inboxSession, onSuccess: onSuccess, onFailure: onFailure) } func track(inAppDelivery message: IterableInAppMessage) { diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 2b0b5936a..005ac4ab1 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -44,35 +44,35 @@ struct IterableRequestProcessor { onFailure: onFailure) } } - + @discardableResult func disableDeviceForCurrentUser(hexToken: String, withOnSuccess onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - return disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult func disableDeviceForAllUsers(hexToken: String, withOnSuccess onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - return disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) + disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) } - + @discardableResult func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateUser", - forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + andFailureHandler: onFailure, + withIdentifier: "updateUser", + forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) } - + @discardableResult func updateEmail(_ newEmail: String, - withToken token: String? = nil, + withToken _: String? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, @@ -88,11 +88,11 @@ struct IterableRequestProcessor { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPurchase", - forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) + andFailureHandler: onFailure, + withIdentifier: "trackPurchase", + forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) } - + @discardableResult func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, @@ -102,41 +102,41 @@ struct IterableRequestProcessor { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPushOpen", - forResult: apiClient.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) + 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 { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackEvent", - forResult: apiClient.track(event: event, dataFields: dataFields)) + andFailureHandler: onFailure, + withIdentifier: "trackEvent", + forResult: apiClient.track(event: event, dataFields: dataFields)) } - + @discardableResult func updateSubscriptions(info: UpdateSubscriptionsInfo, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(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)) + 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, @@ -144,13 +144,56 @@ struct IterableRequestProcessor { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) + return IterableRequestProcessor.call(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 IterableRequestProcessor.call(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 IterableRequestProcessor.call(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 IterableRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", + withIdentifier: "trackInboxSession", forResult: result) } - @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, @@ -169,16 +212,16 @@ struct IterableRequestProcessor { pushServicePlatform: pushServicePlatformString, notificationsEnabled: notificationsEnabled)) } - + @discardableResult private func disableDevice(forAllUsers allUsers: Bool, hexToken: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "disableDevice", - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) } private static func pushServicePlatformToString(_ pushServicePlatform: PushServicePlatform, apnsType: APNSType) -> String { From 2f2984599854ae17f901dd6435c7d2d51c840cf6 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 14:11:14 +0530 Subject: [PATCH 23/76] Move trackInAppDelivery, inAppConsume methods to IterableRequestProcessor. --- swift-sdk/Internal/IterableAPIInternal.swift | 35 +++++++++------ .../Internal/IterableRequestProcessor.swift | 44 ++++++++++++++++--- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index d08a4e902..f6f64c217 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -349,24 +349,31 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { requestProcessor.track(inboxSession: inboxSession, onSuccess: onSuccess, onFailure: onFailure) } - func track(inAppDelivery message: IterableInAppMessage) { - IterableAPIInternal.call(successHandler: IterableAPIInternal.defaultOnSuccess("trackInAppDelivery"), - andFailureHandler: IterableAPIInternal.defaultOnFailure("trackInAppDelivery"), - 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"), - 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"), - 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/Initializers diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index 005ac4ab1..b3ccf91a4 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -181,7 +181,7 @@ struct IterableRequestProcessor { withIdentifier: "trackInAppClose", forResult: result) } - + @discardableResult func track(inboxSession: IterableInboxSession, onSuccess: OnSuccessHandler? = nil, @@ -189,11 +189,45 @@ struct IterableRequestProcessor { let result = apiClient.track(inboxSession: inboxSession) return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInboxSession", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInboxSession", + forResult: result) } - + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + IterableRequestProcessor.call(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 { + IterableRequestProcessor.call(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 IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "inAppConsumeWithSource", + forResult: result) + } + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From 504ef5957e87fc1a6fd52c8a1089495bca9d5c5c Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 14:37:53 +0530 Subject: [PATCH 24/76] Migrate deprecated methods to IterableRequestProcessor --- swift-sdk/Internal/IterableAPIInternal.swift | 19 ++++++++------- .../Internal/IterableRequestProcessor.swift | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index f6f64c217..584d32d0b 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -640,17 +640,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/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index b3ccf91a4..f6c9c5596 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -228,6 +228,29 @@ struct IterableRequestProcessor { forResult: result) } + // MARK: DEPRECATED + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let result = apiClient.track(inAppOpen: messageId) + return IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + IterableRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + } + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From 188eb5b3eaaa6145ea09a176aea9514379fb7af8 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 14:38:16 +0530 Subject: [PATCH 25/76] Formatting --- swift-sdk/Internal/IterableRequestProcessor.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index f6c9c5596..ed6b9df27 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -229,6 +229,7 @@ struct IterableRequestProcessor { } // MARK: DEPRECATED + @discardableResult func trackInAppOpen(_ messageId: String, onSuccess: OnSuccessHandler? = nil, @@ -246,11 +247,11 @@ struct IterableRequestProcessor { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) } - + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, From 52d94c642d877e056d5b1b2c7bd706d824dbb480 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 14:42:29 +0530 Subject: [PATCH 26/76] Remove unused methods --- swift-sdk/Internal/IterableAPIInternal.swift | 23 ------------------- .../IterableAppIntegrationInternal.swift | 8 +++---- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 584d32d0b..24201541b 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -87,29 +87,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?, withToken token: String? = nil) { if email != _email { logoutPreviousUser() diff --git a/swift-sdk/Internal/IterableAppIntegrationInternal.swift b/swift-sdk/Internal/IterableAppIntegrationInternal.swift index faef63b27..3dbc0084a 100644 --- a/swift-sdk/Internal/IterableAppIntegrationInternal.swift +++ b/swift-sdk/Internal/IterableAppIntegrationInternal.swift @@ -100,8 +100,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 +114,8 @@ extension PushTrackerProtocol { messageId: messageId, appAlreadyRunning: appAlreadyRunning, dataFields: dataFields, - onSuccess: IterableAPIInternal.defaultOnSuccess("trackPushOpen"), - onFailure: IterableAPIInternal.defaultOnFailure("trackPushOpen")) + onSuccess: nil, + onFailure: nil) } } From 1d720682a3921d645487ef553398cfa243eb5d56 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 22:12:00 +0530 Subject: [PATCH 27/76] Introduce DirectCallRequestProcessor. --- swift-sdk.xcodeproj/project.pbxproj | 12 +- swift-sdk/Internal/DependencyContainer.swift | 5 + .../Internal/DirectCallRequestProcessor.swift | 320 +++++++++++++++++ swift-sdk/Internal/IterableAPIInternal.swift | 36 +- .../Internal/IterableRequestProcessor.swift | 336 ++++-------------- tests/common/CommonExtensions.swift | 4 + 6 files changed, 425 insertions(+), 288 deletions(-) create mode 100644 swift-sdk/Internal/DirectCallRequestProcessor.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 0650ef8c0..9919e1291 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -76,7 +76,7 @@ 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 */; }; - AC5E888924E1B7CE00752321 /* IterableRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */; }; + AC5E888924E1B7CE00752321 /* DirectCallRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */; }; AC64626B2140AACF0046E1BD /* IterableAPIResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */; }; @@ -182,6 +182,7 @@ 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 /* IterableRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF32BDA24E3EA7C0072E2CC /* IterableRequestProcessor.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 */; }; @@ -398,7 +399,7 @@ 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 = ""; }; - AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequestProcessor.swift; sourceTree = ""; }; + AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectCallRequestProcessor.swift; sourceTree = ""; }; AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPIResponseTests.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 = ""; }; @@ -495,6 +496,7 @@ 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 /* IterableRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequestProcessor.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 = ""; }; @@ -823,7 +825,8 @@ AC5E888724E1B7AD00752321 /* Request Processing */ = { isa = PBXGroup; children = ( - AC5E888824E1B7CE00752321 /* IterableRequestProcessor.swift */, + ACF32BDA24E3EA7C0072E2CC /* IterableRequestProcessor.swift */, + AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -1551,6 +1554,7 @@ files = ( AC31B042232AB53500BE25EB /* InboxImpressionTracker.swift in Sources */, 55D54656239AE5750093ED1E /* LoggingInternal.swift in Sources */, + ACF32BDB24E3EA7C0072E2CC /* IterableRequestProcessor.swift in Sources */, AC426CC4211B5497002EDBE8 /* ServerResponse.swift in Sources */, AC219C49225FD7EB00B98631 /* IterableInboxViewController.swift in Sources */, AC3DD9C82142F3650046F886 /* ClassExtensions.swift in Sources */, @@ -1612,7 +1616,7 @@ AC31B040232AB42100BE25EB /* InboxSessionManager.swift in Sources */, ACE34AB321376B1000691224 /* UserDefaultsLocalStorage.swift in Sources */, AC8E9268246284F800BEB68E /* DataFieldsHelper.swift in Sources */, - AC5E888924E1B7CE00752321 /* IterableRequestProcessor.swift in Sources */, + AC5E888924E1B7CE00752321 /* DirectCallRequestProcessor.swift in Sources */, ACC362C324D21332002C67BA /* IterableTaskError.swift in Sources */, ACC362C124D21272002C67BA /* IterableTaskResult.swift in Sources */, AC2C667E20D3111900D46CC9 /* DateProvider.swift in Sources */, diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index 5df013e12..2775139c1 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 createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor } extension DependencyContainerProtocol { @@ -43,6 +44,10 @@ extension DependencyContainerProtocol { } struct DependencyContainer: DependencyContainerProtocol { + func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + DirectCallRequestProcessor(apiClient: apiClient) + } + func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol { InAppFetcher(apiClient: apiClient) } diff --git a/swift-sdk/Internal/DirectCallRequestProcessor.swift b/swift-sdk/Internal/DirectCallRequestProcessor.swift new file mode 100644 index 000000000..06b050fd1 --- /dev/null +++ b/swift-sdk/Internal/DirectCallRequestProcessor.swift @@ -0,0 +1,320 @@ +// +// 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 DirectCallRequestProcessor: IterableRequestProcessor { + let apiClient: ApiClientProtocol! + + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + // check notificationsEnabled then call register with enabled/not-enabled + notificationStateProvider.notificationsEnabled + .mapFailure(SendRequestError.from(error:)) + .replaceError(with: false) + .flatMap { enabled in + self.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: enabled, + 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 { + DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "updateUser", + forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + } + + @discardableResult + func updateEmail(_ newEmail: String, + withToken _: String? = nil, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + DirectCallRequestProcessor.call(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 { + DirectCallRequestProcessor.call(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 { + DirectCallRequestProcessor.call(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 { + DirectCallRequestProcessor.call(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 { + DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInboxSession", + forResult: result) + } + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + DirectCallRequestProcessor.call(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 { + DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(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 DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + } + + @discardableResult + private func register(registerTokenInfo: RegisterTokenInfo, + notificationsEnabled: Bool, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let pushServicePlatformString = DirectCallRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) + + return DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "registerToken", + forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, + appName: registerTokenInfo.appName, + deviceId: registerTokenInfo.deviceId, + sdkVersion: registerTokenInfo.sdkVersion, + deviceAttributes: registerTokenInfo.deviceAttributes, + pushServicePlatform: pushServicePlatformString, + notificationsEnabled: notificationsEnabled)) + } + + @discardableResult + private func disableDevice(forAllUsers allUsers: Bool, + hexToken: String, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + DirectCallRequestProcessor.call(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + } + + 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 + } + } + + @discardableResult + private static func call(successHandler onSuccess: OnSuccessHandler? = nil, + andFailureHandler onFailure: OnFailureHandler? = nil, + withIdentifier identifier: String, + forResult result: Future) -> Future { + result.onSuccess { json in + if let onSuccess = onSuccess { + onSuccess(json) + } else { + defaultOnSuccess(identifier)(json) + } + }.onError { error in + if let onFailure = onFailure { + onFailure(error.reason, error.data) + } else { + defaultOnFailure(identifier)(error.reason, error.data) + } + } + return result + } + + 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) + } + } +} diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 24201541b..2327373dd 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -141,13 +141,13 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { } hexToken = token.hexString() - let registerTokenInfo = IterableRequestProcessor.RegisterTokenInfo(hexToken: token.hexString(), - appName: appName, - pushServicePlatform: config.pushPlatform, - apnsType: dependencyContainer.apnsTypeChecker.apnsType, - deviceId: deviceId, - deviceAttributes: deviceAttributes, - sdkVersion: localStorage.sdkVersion) + 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, @@ -195,11 +195,13 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { withToken token: String? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - requestProcessor.updateEmail(newEmail, withToken: token, onSuccess: onSuccess, onFailure: onFailure).onSuccess { _ in - // only change email if one is being used + requestProcessor.updateEmail(newEmail, withToken: token, onSuccess: nil, onFailure: nil).onSuccess { json in if self.email != nil { self.setEmail(newEmail, withToken: token) } + onSuccess?(json) + }.onError { error in + onFailure?(error.reason, error.data) } } @@ -266,12 +268,12 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { templateId: NSNumber?, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - let updateSubscriptionsInfo = IterableRequestProcessor.UpdateSubscriptionsInfo(emailListIds: emailListIds, - unsubscribedChannelIds: unsubscribedChannelIds, - unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, - subscribedMessageTypeIds: subscribedMessageTypeIds, - campaignId: campaignId, - templateId: templateId) + let updateSubscriptionsInfo = UpdateSubscriptionsInfo(emailListIds: emailListIds, + unsubscribedChannelIds: unsubscribedChannelIds, + unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, + subscribedMessageTypeIds: subscribedMessageTypeIds, + campaignId: campaignId, + templateId: templateId) return requestProcessor.updateSubscriptions(info: updateSubscriptionsInfo, onSuccess: onSuccess, onFailure: onFailure) } @@ -376,7 +378,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, @@ -385,7 +387,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { }() lazy var requestProcessor: IterableRequestProcessor = { - IterableRequestProcessor(apiClient: self.apiClient) + dependencyContainer.createRequestProcessor(apiClient: apiClient) }() private var deviceAttributes = [String: String]() diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/IterableRequestProcessor.swift index ed6b9df27..530472dc0 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/IterableRequestProcessor.swift @@ -5,335 +5,137 @@ 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. -struct IterableRequestProcessor { - let apiClient: ApiClientProtocol! - - 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? - } - +protocol IterableRequestProcessor { @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - // check notificationsEnabled then call register with enabled/not-enabled - notificationStateProvider.notificationsEnabled - .mapFailure(SendRequestError.from(error:)) - .replaceError(with: false) - .flatMap { enabled in - self.register(registerTokenInfo: registerTokenInfo, - notificationsEnabled: enabled, - onSuccess: onSuccess, - onFailure: onFailure) - } - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func disableDeviceForCurrentUser(hexToken: String, - withOnSuccess onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - disableDevice(forAllUsers: false, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) - } + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func disableDeviceForAllUsers(hexToken: String, - withOnSuccess onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - disableDevice(forAllUsers: true, hexToken: hexToken, onSuccess: onSuccess, onFailure: onFailure) - } + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func updateUser(_ dataFields: [AnyHashable: Any], mergeNestedObjects: Bool, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateUser", - forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func updateEmail(_ newEmail: String, - withToken _: String? = nil, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateEmail", - forResult: apiClient.updateEmail(newEmail: newEmail)) - } + withToken _: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func trackPurchase(_ total: NSNumber, items: [CommerceItem], - dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPurchase", - forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) - } + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func trackPushOpen(_ campaignId: NSNumber, templateId: NSNumber?, messageId: String?, appAlreadyRunning: Bool, - dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPushOpen", - forResult: apiClient.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) - } + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func track(event: String, - dataFields: [AnyHashable: Any]? = nil, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackEvent", - forResult: apiClient.track(event: event, dataFields: dataFields)) - } + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func updateSubscriptions(info: UpdateSubscriptionsInfo, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(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)) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @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 IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) - } + inboxSessionId: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func trackInAppClick(_ message: IterableInAppMessage, - location: InAppLocation = .inApp, - inboxSessionId: String? = nil, + location: InAppLocation, + inboxSessionId: String?, 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 IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: result) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @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 IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClose", - forResult: result) - } - + location: InAppLocation, + inboxSessionId: String?, + source: InAppCloseSource?, + clickedUrl: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func track(inboxSession: IterableInboxSession, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - let result = apiClient.track(inboxSession: inboxSession) - - return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInboxSession", - forResult: result) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func track(inAppDelivery message: IterableInAppMessage, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppDelivery", - forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func inAppConsume(_ messageId: String, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsume", - forResult: apiClient.inAppConsume(messageId: messageId)) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @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 IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsumeWithSource", - forResult: result) - } + location: InAppLocation, + source: InAppDeleteSource?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future // MARK: DEPRECATED @discardableResult func trackInAppOpen(_ messageId: String, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - let result = apiClient.track(inAppOpen: messageId) - return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future @discardableResult func trackInAppClick(_ messageId: String, clickedUrl: String, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) - } - - @discardableResult - private func register(registerTokenInfo: RegisterTokenInfo, - notificationsEnabled: Bool, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - let pushServicePlatformString = IterableRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) - - return IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "registerToken", - forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, - appName: registerTokenInfo.appName, - deviceId: registerTokenInfo.deviceId, - sdkVersion: registerTokenInfo.sdkVersion, - deviceAttributes: registerTokenInfo.deviceAttributes, - pushServicePlatform: pushServicePlatformString, - notificationsEnabled: notificationsEnabled)) - } - - @discardableResult - private func disableDevice(forAllUsers allUsers: Bool, - hexToken: String, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Future { - IterableRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "disableDevice", - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) - } - - 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 - } - } - - @discardableResult - private static func call(successHandler onSuccess: OnSuccessHandler? = nil, - andFailureHandler onFailure: OnFailureHandler? = nil, - withIdentifier identifier: String, - forResult result: Future) -> Future { - result.onSuccess { json in - if let onSuccess = onSuccess { - onSuccess(json) - } else { - defaultOnSuccess(identifier)(json) - } - }.onError { error in - if let onFailure = onFailure { - onFailure(error.reason, error.data) - } else { - defaultOnFailure(identifier)(error.reason, error.data) - } - } - return result - } - - 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) - } - } + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future } diff --git a/tests/common/CommonExtensions.swift b/tests/common/CommonExtensions.swift index fb3bf17e3..1d64a629d 100644 --- a/tests/common/CommonExtensions.swift +++ b/tests/common/CommonExtensions.swift @@ -109,6 +109,10 @@ class MockDependencyContainer: DependencyContainerProtocol { func createInAppFetcher(apiClient _: ApiClientProtocol) -> InAppFetcherProtocol { inAppFetcher } + + func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + DirectCallRequestProcessor(apiClient: apiClient) + } } extension IterableAPI { From eb100c487a4e58f02033e06e35a9f3981615a653 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 12 Aug 2020 22:19:17 +0530 Subject: [PATCH 28/76] Fix E2EDependency container. --- tests/endpoint-tests/IterableAPISupport.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/endpoint-tests/IterableAPISupport.swift b/tests/endpoint-tests/IterableAPISupport.swift index 8798a9653..599a01e4b 100644 --- a/tests/endpoint-tests/IterableAPISupport.swift +++ b/tests/endpoint-tests/IterableAPISupport.swift @@ -81,6 +81,10 @@ class E2EDependencyContainer: DependencyContainerProtocol { func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol { InAppFetcher(apiClient: apiClient) } + + func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + DirectCallRequestProcessor(apiClient: apiClient) + } } extension IterableAPIInternal { From 02d8343413b7e14e60b5636dc3af33e4e5207511 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 14 Aug 2020 10:59:38 +0530 Subject: [PATCH 29/76] 1. TaskProcessor handle success and failure for network unavailable. 2. Return status code and error from NetworkSession. --- .../IterableAPICallTaskProcessor.swift | 24 +++- swift-sdk/Internal/IterableTaskResult.swift | 15 +-- swift-sdk/Internal/NetworkHelper.swift | 23 ++-- swift-sdk/Internal/Promise.swift | 4 +- .../TaskProcessorTests.swift | 119 +++++++++++++++++- 5 files changed, 164 insertions(+), 21 deletions(-) diff --git a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift index 8b47c8ff4..a33c1c573 100644 --- a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -21,12 +21,30 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { let result = Promise() NetworkHelper.sendRequest(urlRequest, usingSession: networkSession) .onSuccess { json in - result.resolve(with: .success(APICallTaskSuccess(json: json))) + result.resolve(with: .success(detail: APICallTaskSuccessDetail(json: json))) } - .onError { networkError in - result.resolve(with: .failure(APICallTaskFailure(responseCode: nil, reason: networkError.reason, data: networkError.data))) + .onError { sendRequestError in + if IterableAPICallTaskProcessor.isNetworkUnavailable(sendRequestError: sendRequestError) { + let failureDetail = APICallTaskFailureDetail(httpStatusCode: sendRequestError.httpStatusCode, + reason: sendRequestError.reason, + data: sendRequestError.data) + result.resolve(with: .failureWithRetry(retryAfter: nil, detail: failureDetail)) + } else { + let failureDetail = APICallTaskFailureDetail(httpStatusCode: sendRequestError.httpStatusCode, + reason: sendRequestError.reason, + data: sendRequestError.data) + result.resolve(with: .failureWithNoRetry(detail: failureDetail)) + } } return result } + + private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool { + if let originalError = sendRequestError.originalError as? LocalizedError { + return originalError.localizedDescription.lowercased().contains("unavailable") + } else { + return false + } + } } diff --git a/swift-sdk/Internal/IterableTaskResult.swift b/swift-sdk/Internal/IterableTaskResult.swift index 61aafbb51..f2e325166 100644 --- a/swift-sdk/Internal/IterableTaskResult.swift +++ b/swift-sdk/Internal/IterableTaskResult.swift @@ -6,20 +6,21 @@ import Foundation enum IterableTaskResult { - case success(TaskSuccess) - case failure(TaskFailure) + case success(detail: TaskSuccessDetail?) + case failureWithRetry(retryAfter: TimeInterval?, detail: TaskFailureDetail?) + case failureWithNoRetry(detail: TaskFailureDetail?) } -protocol TaskSuccess {} +protocol TaskSuccessDetail {} -struct APICallTaskSuccess: TaskSuccess { +struct APICallTaskSuccessDetail: TaskSuccessDetail { let json: SendRequestValue } -protocol TaskFailure {} +protocol TaskFailureDetail {} -struct APICallTaskFailure: TaskFailure { - let responseCode: Int? +struct APICallTaskFailureDetail: TaskFailureDetail { + let httpStatusCode: Int? let reason: String? let data: Data? } diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index f880c4813..437cf663f 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -10,10 +10,17 @@ typealias SendRequestValue = [AnyHashable: Any] struct SendRequestError: Error { let reason: String? let data: Data? + let httpStatusCode: Int? + let originalError: Error? - init(reason: String? = nil, data: Data? = nil) { + 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 } static func createErroredFuture(reason: String? = nil) -> Future { @@ -117,7 +124,7 @@ struct NetworkHelper { response: URLResponse?, error: Error?) -> Result { if let error = error { - return .failure(SendRequestError(reason: "\(error.localizedDescription)", data: data)) + return .failure(SendRequestError(reason: "\(error.localizedDescription)", data: data, originalError: error)) } guard let response = response as? HTTPURLResponse else { @@ -141,7 +148,7 @@ struct NetworkHelper { } if responseCode == 401 { - return .failure(SendRequestError(reason: "Invalid API Key", data: data)) + return .failure(SendRequestError(reason: "Invalid API Key", data: data, httpStatusCode: responseCode)) } else if responseCode >= 400 { var reason = "Invalid Request" if let jsonDict = json as? [AnyHashable: Any], let msgFromDict = jsonDict["msg"] as? String { @@ -150,7 +157,7 @@ struct NetworkHelper { reason = "Internal Server Error" } - return .failure(SendRequestError(reason: reason, data: data)) + return .failure(SendRequestError(reason: reason, data: data, httpStatusCode: responseCode)) } else if responseCode == 200 { if let data = data, data.count > 0 { if let jsonError = jsonError { @@ -159,17 +166,17 @@ struct NetworkHelper { reason = "Could not parse json: \(stringValue), error: \(jsonError.localizedDescription)" } - return .failure(SendRequestError(reason: reason, data: data)) + 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)) + return .failure(SendRequestError(reason: "Response is not a dictionary", data: data, httpStatusCode: responseCode)) } } else { - return .failure(SendRequestError(reason: "No data received", data: data)) + return .failure(SendRequestError(reason: "No data received", data: data, httpStatusCode: responseCode)) } } else { - return .failure(SendRequestError(reason: "Received non-200 response: \(responseCode)", data: data)) + return .failure(SendRequestError(reason: "Received non-200 response: \(responseCode)", data: data, httpStatusCode: responseCode)) } } diff --git a/swift-sdk/Internal/Promise.swift b/swift-sdk/Internal/Promise.swift index dde72fb56..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") } } } diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift index 1b977226d..bf6d5cbf0 100644 --- a/tests/offline-events-tests/TaskProcessorTests.swift +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -58,7 +58,124 @@ class TaskProcessorTests: XCTestCase { 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: "Network Unavailable") + 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() + let taskProcessor = "APICallTaskProcessor" + try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + 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() From d4aa0fcf65766973f25fdfd4345c5b7fe4b99184 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 18 Aug 2020 21:19:30 +0530 Subject: [PATCH 30/76] First version of IterableTaskRunner and IterableTaskScheduler. --- swift-sdk.xcodeproj/project.pbxproj | 24 +++++ swift-sdk/Internal/InAppManager.swift | 8 -- .../IterableAPICallTaskProcessor.swift | 22 ++--- .../IterableCoreDataPersistence.swift | 8 ++ .../Internal/IterableNotifications.swift | 21 +++++ swift-sdk/Internal/IterablePersistence.swift | 4 + .../Internal/IterableTaskProcessor.swift | 8 ++ swift-sdk/Internal/IterableTaskResult.swift | 10 +- swift-sdk/Internal/IterableTaskRunner.swift | 91 +++++++++++++++++++ .../Internal/IterableTaskScheduler.swift | 24 +++++ swift-sdk/Internal/NetworkHelper.swift | 10 +- .../TaskRunnerTests.swift | 76 ++++++++++++++++ 12 files changed, 277 insertions(+), 29 deletions(-) create mode 100644 swift-sdk/Internal/IterableNotifications.swift create mode 100644 swift-sdk/Internal/IterableTaskRunner.swift create mode 100644 swift-sdk/Internal/IterableTaskScheduler.swift create mode 100644 tests/offline-events-tests/TaskRunnerTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 9919e1291..b84d4dfb2 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -39,6 +39,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 */; }; @@ -52,6 +54,8 @@ 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 */; }; @@ -361,6 +365,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 = ""; }; @@ -376,6 +382,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 = ""; }; @@ -650,6 +658,14 @@ name = "Helper Files"; sourceTree = ""; }; + AC1AA1C724EBB39500F29C6B /* Notification Center */ = { + isa = PBXGroup; + children = ( + AC1AA1C824EBB3C300F29C6B /* IterableNotifications.swift */, + ); + name = "Notification Center"; + sourceTree = ""; + }; AC219C4A225FD7F900B98631 /* Inbox UI */ = { isa = PBXGroup; children = ( @@ -846,6 +862,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + AC1AA1C724EBB39500F29C6B /* Notification Center */, AC5E888724E1B7AD00752321 /* Request Processing */, ACC362BB24D21153002C67BA /* Task Processing */, AC50865124C60133001DC132 /* Persistence */, @@ -1013,6 +1030,8 @@ AC50865724C60426001DC132 /* IterableTask.swift */, ACC362C024D21272002C67BA /* IterableTaskResult.swift */, ACC362C224D21332002C67BA /* IterableTaskError.swift */, + AC1AA1C524EBB2DC00F29C6B /* IterableTaskRunner.swift */, + AC2AED4324EBC905000EE5F3 /* IterableTaskScheduler.swift */, ); name = "Task Processing"; sourceTree = ""; @@ -1099,6 +1118,7 @@ ACFD5ABC24C8200C008E497A /* Info.plist */, ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, + AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1560,6 +1580,7 @@ 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 */, @@ -1590,6 +1611,7 @@ 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 */, @@ -1625,6 +1647,7 @@ AC426226238C27DD00164121 /* IterableInboxCell+Layout.swift in Sources */, AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */, AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */, + AC1AA1C624EBB2DC00F29C6B /* IterableTaskRunner.swift in Sources */, ACFD5AC824C8290E008E497A /* IterableTaskManagedObject.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1775,6 +1798,7 @@ ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, ACC362C524D2C190002C67BA /* TaskProcessorTests.swift in Sources */, + AC2AED4224EBC60C000EE5F3 /* TaskRunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/swift-sdk/Internal/InAppManager.swift b/swift-sdk/Internal/InAppManager.swift index c4c3521ff..01f035089 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/IterableAPICallTaskProcessor.swift b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift index a33c1c573..eaf7b2271 100644 --- a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -9,6 +9,7 @@ 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") } @@ -20,20 +21,17 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { let result = Promise() NetworkHelper.sendRequest(urlRequest, usingSession: networkSession) - .onSuccess { json in - result.resolve(with: .success(detail: APICallTaskSuccessDetail(json: json))) + .onSuccess { sendRequestValue in + ITBInfo("Task finished successfully") + result.resolve(with: .success(detail: sendRequestValue)) } .onError { sendRequestError in if IterableAPICallTaskProcessor.isNetworkUnavailable(sendRequestError: sendRequestError) { - let failureDetail = APICallTaskFailureDetail(httpStatusCode: sendRequestError.httpStatusCode, - reason: sendRequestError.reason, - data: sendRequestError.data) - result.resolve(with: .failureWithRetry(retryAfter: nil, detail: failureDetail)) + ITBInfo("Network is unavailable") + result.resolve(with: .failureWithRetry(retryAfter: nil, detail: sendRequestError)) } else { - let failureDetail = APICallTaskFailureDetail(httpStatusCode: sendRequestError.httpStatusCode, - reason: sendRequestError.reason, - data: sendRequestError.data) - result.resolve(with: .failureWithNoRetry(detail: failureDetail)) + ITBInfo("Unrecoverable error") + result.resolve(with: .failureWithNoRetry(detail: sendRequestError)) } } @@ -41,8 +39,8 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { } private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool { - if let originalError = sendRequestError.originalError as? LocalizedError { - return originalError.localizedDescription.lowercased().contains("unavailable") + if let originalError = sendRequestError.originalError { + return originalError.localizedDescription.lowercased().contains("offline") } else { return false } diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 659958011..16aaa712d 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -120,6 +120,14 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { 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 diff --git a/swift-sdk/Internal/IterableNotifications.swift b/swift-sdk/Internal/IterableNotifications.swift new file mode 100644 index 000000000..10db23394 --- /dev/null +++ b/swift-sdk/Internal/IterableNotifications.swift @@ -0,0 +1,21 @@ +// +// 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 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") +} + diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index aa21db205..c89910b40 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -41,6 +41,10 @@ protocol IterablePersistenceContext { func deleteAllTasks() throws func save() throws + + func perform(_ block: @escaping () -> Void) + + func performAndWait(_ block: () -> Void) } protocol IterablePersistenceContextProvider { diff --git a/swift-sdk/Internal/IterableTaskProcessor.swift b/swift-sdk/Internal/IterableTaskProcessor.swift index c55013dbf..88b6ced2c 100644 --- a/swift-sdk/Internal/IterableTaskProcessor.swift +++ b/swift-sdk/Internal/IterableTaskProcessor.swift @@ -5,6 +5,14 @@ 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 index f2e325166..a5b3dcea3 100644 --- a/swift-sdk/Internal/IterableTaskResult.swift +++ b/swift-sdk/Internal/IterableTaskResult.swift @@ -13,14 +13,8 @@ enum IterableTaskResult { protocol TaskSuccessDetail {} -struct APICallTaskSuccessDetail: TaskSuccessDetail { - let json: SendRequestValue -} +extension SendRequestValue: TaskSuccessDetail {} protocol TaskFailureDetail {} -struct APICallTaskFailureDetail: TaskFailureDetail { - let httpStatusCode: Int? - let reason: String? - let data: Data? -} +extension SendRequestError: TaskFailureDetail {} diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift new file mode 100644 index 000000000..1baba9e2e --- /dev/null +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -0,0 +1,91 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +@available(iOS 10.0, *) +class IterableTaskRunner { + // TODO: Move to `DependencyContainer` after we remove iOS 9 support + init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), + persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider()) { + self.networkSession = networkSession + self.persistenceContextProvider = persistenceContextProvider + } + + func start() throws { + ITBInfo() + shouldExecute = true + persistenceContext.perform { + while self.shouldExecute { + try? self.execute() + Thread.sleep(forTimeInterval: 1.0) + } + } + } + + func stop() throws { + ITBInfo() + shouldExecute = false + } + + func execute() throws { + ITBInfo() + let tasks = try persistenceContext.findAllTasks() + ITBInfo("numTasks: \(tasks.count)") + for task in tasks { + try execute(task: task).wait() + } + } + + @discardableResult + func execute(task: IterableTask) throws -> Future { + ITBInfo("executing taskId: \(task.id)") + let result = Promise() + let processor = IterableAPICallTaskProcessor(networkSession: networkSession) + try processor.process(task: task).onSuccess { taskResult in + switch taskResult { + case let .success(detail: detail): + ITBInfo("task: \(task.id) succeeded") + self.deleteTask(task: task) + if let successDetail = detail as? SendRequestValue { + var userInfo = [AnyHashable: Any]() + userInfo["taskId"] = task.id + userInfo["sendRequestValue"] = successDetail + NotificationCenter.default.post(name: .iterableTaskFinishedWithSuccess, object: self, userInfo: userInfo) + } + case let .failureWithNoRetry(detail: detail): + ITBInfo("task: \(task.id) failed with no retry.") + self.deleteTask(task: task) + if let failureDetail = detail as? SendRequestError { + var userInfo = [AnyHashable: Any]() + userInfo["taskId"] = task.id + userInfo["sendRequestError"] = failureDetail + NotificationCenter.default.post(name: .iterableTaskFinishedWithNoRetry, object: self, userInfo: userInfo) + } + case .failureWithRetry: + ITBInfo("task: \(task.id) processed with retry") + break + } + result.resolve(with: ()) + } + return result + } + + private func deleteTask(task: IterableTask) { + do { + try persistenceContext.delete(task: task) + try persistenceContext.save() + } catch let error { + ITBError(error.localizedDescription) + } + } + + private var shouldExecute = true + private let networkSession: NetworkSessionProtocol + private let persistenceContextProvider: IterablePersistenceContextProvider + 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..7b46771bd --- /dev/null +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -0,0 +1,24 @@ +// +// Created by Tapash Majumder on 8/18/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +@available(iOS 10.0, *) +struct IterableTaskScheduler { + func schedule(apiCallRequest: IterableAPICallRequest, context: IterableTaskContext) throws -> String { + // persist data + let taskId = IterableUtil.generateUUID() + let taskProcessor = "APICallTaskProcessor" + let data = try JSONEncoder().encode(apiCallRequest) + + let persistenceContext = persistenceProvider.newBackgroundContext() + try persistenceContext.create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + try persistenceContext.save() + + return taskId + } + + private let persistenceProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider() +} diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 437cf663f..42bda9233 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -87,9 +87,11 @@ struct NetworkHelper { static func sendRequest(_ request: URLRequest, usingSession networkSession: NetworkSessionProtocol) -> Future { #if NETWORK_DEBUG + let requestId = IterableUtil.generateUUID() print() print("====================================================>") print("sending request: \(request)") + print("requestId: \(requestId)") if let headers = request.allHTTPHeaderFields { print("headers:") print(headers) @@ -111,15 +113,21 @@ struct NetworkHelper { switch result { case let .success(value): + #if NETWORK_DEBUG + print("request with requestId: \(requestId) successfully sent") + #endif promise.resolve(with: value) case let .failure(error): + #if NETWORK_DEBUG + print("request with id: \(requestId) errored") + #endif promise.reject(with: error) } } return promise } - + static func createResultFromNetworkResponse(data: Data?, response: URLResponse?, error: Error?) -> Result { diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift new file mode 100644 index 000000000..beecf33be --- /dev/null +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -0,0 +1,76 @@ +// +// 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 { + super.setUp() + + try persistenceContext.deleteAllTasks() + try persistenceContext.save() + + taskExecutor = IterableTaskRunner(networkSession: MockNetworkSession()) + try taskExecutor.start() + } + + override func tearDownWithError() throws { + try taskExecutor.stop() + Thread.sleep(forTimeInterval: 2.0) + } + + func testTrackEvent() throws { + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + let expectation1 = expectation(description: #function) + 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 { + XCTFail("Could not create trackEvent request") + return + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: Endpoint.api, + auth: auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + + do { + let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, + context: IterableTaskContext(blocking: true)) + XCTAssertNotNil(taskId) + expectation1.fulfill() + } catch let error { + ITBError(error.localizedDescription) + XCTFail(error.localizedDescription) + } + + Thread.sleep(forTimeInterval: 5.0) + wait(for: [expectation1], timeout: 1000.0) + } + + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") + + private lazy var persistenceContext: IterablePersistenceContext = { + let provider = CoreDataPersistenceContextProvider() + return provider.mainQueueContext() + } () + + private var taskExecutor: IterableTaskRunner! +} + +extension TaskRunnerTests: AuthProvider { + var auth: Auth { + Auth(userId: nil, email: "user@example.com", authToken: nil) + } +} From 8650bd20e08a7135ed1de63d29e4280971e7e7d1 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 19 Aug 2020 12:29:06 +0530 Subject: [PATCH 31/76] Change persistence layer with latest data model needed for TaskRunner. --- host-app/Info.plist | 2 +- swift-sdk.xcodeproj/project.pbxproj | 8 +- .../IterableCoreDataPersistence.swift | 8 +- swift-sdk/Internal/IterablePersistence.swift | 2 +- swift-sdk/Internal/IterableTask.swift | 74 ++++++++++++------- .../Internal/IterableTaskManagedObject.swift | 17 +++-- .../Internal/IterableTaskScheduler.swift | 12 ++- swift-sdk/Internal/PersistenceHelper.swift | 34 ++++++--- .../IterableDataModel.xcdatamodel/contents | 22 ++++-- .../TaskProcessorTests.swift | 16 ++-- .../offline-events-tests/TasksCRUDTests.swift | 49 +++++++----- ui-tests-app/Info.plist | 2 +- 12 files changed, 158 insertions(+), 88 deletions(-) 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 b84d4dfb2..ec7d1b04e 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -2444,7 +2444,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; @@ -2465,7 +2465,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "iterable.host-app"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "host-app"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -2529,7 +2529,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; @@ -2550,7 +2550,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; diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 16aaa712d..d7c01e657 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -69,7 +69,7 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { } PersistenceHelper.copy(from: task, to: taskManagedObject) - taskManagedObject.created = dateProvider.currentDate + taskManagedObject.createdAt = dateProvider.currentDate return PersistenceHelper.task(from: taskManagedObject) } @@ -79,7 +79,7 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { } PersistenceHelper.copy(from: task, to: taskManagedObject) - taskManagedObject.modified = dateProvider.currentDate + taskManagedObject.modifiedAt = dateProvider.currentDate return PersistenceHelper.task(from: taskManagedObject) } @@ -87,8 +87,8 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { try deleteTask(withId: task.id) } - func createTask(id: String, processor: String) throws -> IterableTask { - try create(task: IterableTask(id: id, processor: processor)) + func createTask(id: String, type: IterableTaskType) throws -> IterableTask { + try create(task: IterableTask(id: id, type: type, scheduledAt: dateProvider.currentDate, requestedAt: dateProvider.currentDate)) } func findTask(withId id: String) throws -> IterableTask? { diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index c89910b40..1a72d15d1 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -30,7 +30,7 @@ protocol IterablePersistenceContext { func delete(task: IterableTask) throws @discardableResult - func createTask(id: String, processor: String) throws -> IterableTask + func createTask(id: String, type: IterableTaskType) throws -> IterableTask func findTask(withId id: String) throws -> IterableTask? diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index 91942ce4d..b3072c75d 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -6,49 +6,73 @@ import Foundation struct IterableTask { + static let currentVersion = 1 + let id: String - let created: Date? - let modified: Date? - let processor: String + let version: Int + let createdAt: Date? + let modifiedAt: Date? + let type: IterableTaskType let attempts: Int - let lastAttempt: Date? + let lastAttemptedAt: Date? let processing: Bool - let scheduleTime: Date? + let scheduledAt: Date let data: Data? + let failed: Bool + let blocking: Bool + let requestedAt: Date + let taskFailureData: Data? init(id: String, - created: Date? = nil, - modified: Date? = nil, - processor: String, + version: Int = IterableTask.currentVersion, + createdAt: Date? = nil, + modifiedAt: Date? = nil, + type: IterableTaskType, attempts: Int = 0, - lastAttempt: Date? = nil, + lastAttemptedAt: Date? = nil, processing: Bool = false, - scheduleTime: Date? = nil, - data: Data? = nil) { + scheduledAt: Date, + data: Data? = nil, + failed: Bool = false, + blocking: Bool = true, + requestedAt: Date, + taskFailureData: Data? = nil) { self.id = id - self.created = created - self.modified = modified - self.processor = processor + self.version = version + self.createdAt = createdAt + self.modifiedAt = modifiedAt + self.type = type self.attempts = attempts - self.lastAttempt = lastAttempt + self.lastAttemptedAt = lastAttemptedAt self.processing = processing - self.scheduleTime = scheduleTime + self.scheduledAt = scheduledAt self.data = data + self.failed = failed + self.blocking = blocking + self.requestedAt = requestedAt + self.taskFailureData = taskFailureData } func updated(attempts: Int? = nil, - lastAttempt: Date? = nil, + lastAttemptedAt: Date? = nil, processing: Bool? = nil, - scheduleTime: Date? = nil, - data: Data? = nil) -> IterableTask { + scheduledAt: Date? = nil, + data: Data? = nil, + failed: Bool? = nil, + taskFailureData: Data? = nil) -> IterableTask { IterableTask(id: id, - created: created, - modified: modified, - processor: processor, + version: version, + createdAt: createdAt, + modifiedAt: modifiedAt, + type: type, attempts: attempts ?? self.attempts, - lastAttempt: lastAttempt ?? self.lastAttempt, + lastAttemptedAt: lastAttemptedAt ?? self.lastAttemptedAt, processing: processing ?? self.processing, - scheduleTime: scheduleTime ?? self.scheduleTime, - data: data ?? self.data) + 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/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift index 3bcaaf821..e72e24569 100644 --- a/swift-sdk/Internal/IterableTaskManagedObject.swift +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -17,12 +17,17 @@ extension IterableTaskManagedObject { } @NSManaged public var attempts: Int64 - @NSManaged public var created: Date? + @NSManaged public var createdAt: Date? + @NSManaged public var modifiedAt: Date? @NSManaged public var data: Data? - @NSManaged public var id: String? - @NSManaged public var lastAttempt: Date? - @NSManaged public var modified: Date? + @NSManaged public var id: String + @NSManaged public var lastAttemptedAt: Date? @NSManaged public var processing: Bool - @NSManaged public var processor: String? - @NSManaged public var scheduleTime: Date? + @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/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift index 7b46771bd..09625516e 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -7,14 +7,20 @@ import Foundation @available(iOS 10.0, *) struct IterableTaskScheduler { - func schedule(apiCallRequest: IterableAPICallRequest, context: IterableTaskContext) throws -> String { + // TODO: @tqm Use DateProvider + func schedule(apiCallRequest: IterableAPICallRequest, + context: IterableTaskContext, + scheduledAt: Date = Date()) throws -> String { // persist data let taskId = IterableUtil.generateUUID() - let taskProcessor = "APICallTaskProcessor" let data = try JSONEncoder().encode(apiCallRequest) let persistenceContext = persistenceProvider.newBackgroundContext() - try persistenceContext.create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + try persistenceContext.create(task: IterableTask(id: taskId, + type: .apiCall, + scheduledAt: scheduledAt, + data: data, + requestedAt: Date())) try persistenceContext.save() return taskId diff --git a/swift-sdk/Internal/PersistenceHelper.swift b/swift-sdk/Internal/PersistenceHelper.swift index 8746fd1de..1efe685f0 100644 --- a/swift-sdk/Internal/PersistenceHelper.swift +++ b/swift-sdk/Internal/PersistenceHelper.swift @@ -7,26 +7,36 @@ import Foundation struct PersistenceHelper { static func task(from: IterableTaskManagedObject) -> IterableTask { - IterableTask(id: from.id!, - created: from.created, - modified: from.modified, - processor: from.processor!, + IterableTask(id: from.id, + version: Int(from.version), + createdAt: from.createdAt, + modifiedAt: from.modifiedAt, + type: IterableTaskType(rawValue: from.type) ?? .apiCall, attempts: Int(from.attempts), - lastAttempt: from.lastAttempt, + lastAttemptedAt: from.lastAttemptedAt, processing: from.processing, - scheduleTime: from.scheduleTime, - data: from.data) + 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.created = from.created - to.modified = from.modified - to.processor = from.processor + to.version = Int64(from.version) + to.createdAt = from.createdAt + to.modifiedAt = from.modifiedAt + to.type = from.type.rawValue to.attempts = Int64(from.attempts) - to.lastAttempt = from.lastAttempt + to.lastAttemptedAt = from.lastAttemptedAt to.processing = from.processing - to.scheduleTime = from.scheduleTime + 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/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents index db0ce2892..8d1eec63b 100644 --- a/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents +++ b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents @@ -1,20 +1,28 @@ - + - + + + - - + + - - + + + + + + + + - + \ No newline at end of file diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift index bf6d5cbf0..03673b5f3 100644 --- a/tests/offline-events-tests/TaskProcessorTests.swift +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -37,8 +37,11 @@ class TaskProcessorTests: XCTestCase { // persist data let taskId = IterableUtil.generateUUID() - let taskProcessor = "APICallTaskProcessor" - try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, + type: .apiCall, + scheduledAt: Date(), + data: data, + requestedAt: Date())) try persistenceProvider.mainQueueContext().save() // load data @@ -90,7 +93,7 @@ class TaskProcessorTests: XCTestCase { let expectation1 = expectation(description: #function) let task = try createSampleTask()! - let networkError = IterableError.general(description: "Network Unavailable") + 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) @@ -165,8 +168,11 @@ class TaskProcessorTests: XCTestCase { // persist data let taskId = IterableUtil.generateUUID() - let taskProcessor = "APICallTaskProcessor" - try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId, processor: taskProcessor, data: data)) + 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) diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index 78a42e9d9..c032e6603 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -11,31 +11,37 @@ class TasksCRUDTests: XCTestCase { func testCreate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - let taskProcessor = "Processor1" - let task = try context.createTask(id: taskId, processor: taskProcessor) + let task = try context.createTask(id: taskId, type: .apiCall) try context.save() XCTAssertEqual(task.id, taskId) - XCTAssertEqual(task.processor, taskProcessor) + XCTAssertEqual(task.type, .apiCall) let newContext = persistenceProvider.mainQueueContext() let found = try newContext.findTask(withId: taskId)! XCTAssertEqual(found.id, taskId) - XCTAssertEqual(found.processor, taskProcessor) + XCTAssertEqual(found.type, .apiCall) } func testUpdate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - let taskProcessor = "Processor1" - let task = try context.createTask(id: taskId, processor: taskProcessor) + let task = try context.createTask(id: taskId, type: .apiCall) try context.save() let attempts = 2 - let lastAttempt = Date() + let lastAttemptedAt = Date() let processing = true - let scheduleTime = Date() + let scheduledAt = Date() let data = Data(repeating: 1, count: 20) - let updatedTask = task.updated(attempts: attempts, lastAttempt: lastAttempt, processing: processing, scheduleTime: scheduleTime, data: data) + 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() @@ -43,26 +49,31 @@ class TasksCRUDTests: XCTestCase { let newContext = persistenceProvider.mainQueueContext() let found = try newContext.findTask(withId: taskId)! XCTAssertEqual(found.id, taskId) - XCTAssertEqual(found.processor, taskProcessor) - XCTAssertNotNil(found.created) - XCTAssertNotNil(found.modified) + XCTAssertEqual(found.version, task.version) + XCTAssertEqual(found.type, .apiCall) + XCTAssertNotNil(found.createdAt) + XCTAssertNotNil(found.modifiedAt) XCTAssertEqual(found.attempts, attempts) - XCTAssertEqual(found.lastAttempt, lastAttempt) - XCTAssertEqual(found.scheduleTime, scheduleTime) + 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() - let taskProcessor = "Processor1" - try context.createTask(id: taskId, processor: taskProcessor) + try context.createTask(id: taskId, type: .apiCall) try context.save() let newContext = persistenceProvider.mainQueueContext() let found = try newContext.findTask(withId: taskId)! XCTAssertEqual(found.id, taskId) - XCTAssertEqual(found.processor, taskProcessor) + XCTAssertEqual(found.type, .apiCall) try context.delete(task: found) try context.save() @@ -78,8 +89,8 @@ class TasksCRUDTests: XCTestCase { let tasks = try context.findAllTasks() XCTAssertEqual(tasks.count, 0) - try context.createTask(id: IterableUtil.generateUUID(), processor: "First Processor") - try context.createTask(id: IterableUtil.generateUUID(), processor: "Second Processor") + try context.createTask(id: IterableUtil.generateUUID(), type: .apiCall) + try context.createTask(id: IterableUtil.generateUUID(), type: .apiCall) try context.save() let newTasks = try context.findAllTasks() 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 From ff9d133c83c6a259aa51c8b740bc9fef8a8013fa Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 19 Aug 2020 17:20:41 +0530 Subject: [PATCH 32/76] Choose next task by scheduledAt. --- swift-sdk/Internal/CoreDataUtil.swift | 16 +++++- .../IterableCoreDataPersistence.swift | 29 +++++++--- swift-sdk/Internal/IterablePersistence.swift | 2 + .../Internal/IterableTaskManagedObject.swift | 2 +- swift-sdk/Internal/IterableTaskRunner.swift | 2 +- tests/common/CommonMocks.swift | 4 ++ .../offline-events-tests/TasksCRUDTests.swift | 53 ++++++++++++++++++- .../IterableNotificationResponseTests.swift | 4 -- 8 files changed, 98 insertions(+), 14 deletions(-) diff --git a/swift-sdk/Internal/CoreDataUtil.swift b/swift-sdk/Internal/CoreDataUtil.swift index 1ffeaa7db..e0e8891fd 100644 --- a/swift-sdk/Internal/CoreDataUtil.swift +++ b/swift-sdk/Internal/CoreDataUtil.swift @@ -30,7 +30,21 @@ struct CoreDataUtil { 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 { diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index d7c01e657..eb7fa82d3 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -8,8 +8,16 @@ import Foundation enum PersistenceConst { static let dataModelFileName = "IterableDataModel" - enum EntityName { - static let task = "IterableTaskManagedObject" + + enum Entity { + enum Task { + static let name = "IterableTaskManagedObject" + + enum Column { + static let id = "id" + static let scheduledAt = "scheduledAt" + } + } } } @@ -91,6 +99,15 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { try create(task: IterableTask(id: id, type: type, scheduledAt: dateProvider.currentDate, requestedAt: dateProvider.currentDate)) } + 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 @@ -106,13 +123,13 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { } func findAllTasks() throws -> [IterableTask] { - let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.EntityName.task) + 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.EntityName.task) + let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) taskManagedObjects.forEach { managedObjectContext.delete($0) } } @@ -132,10 +149,10 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { private let dateProvider: DateProviderProtocol private func findTaskManagedObject(id: String) throws -> IterableTaskManagedObject? { - try CoreDataUtil.findEntitiyByColumn(context: managedObjectContext, entity: PersistenceConst.EntityName.task, columnName: "id", columnValue: id) + 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.EntityName.task) + CoreDataUtil.create(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) } } diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index 1a72d15d1..77daa7fd6 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -36,6 +36,8 @@ protocol IterablePersistenceContext { func deleteTask(withId id: String) throws + func nextTask() throws -> IterableTask? + func findAllTasks() throws -> [IterableTask] func deleteAllTasks() throws diff --git a/swift-sdk/Internal/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift index e72e24569..132d9817c 100644 --- a/swift-sdk/Internal/IterableTaskManagedObject.swift +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -13,7 +13,7 @@ public class IterableTaskManagedObject: NSManagedObject {} extension IterableTaskManagedObject { @nonobjc public class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: PersistenceConst.EntityName.task) + NSFetchRequest(entityName: PersistenceConst.Entity.Task.name) } @NSManaged public var attempts: Int64 diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 1baba9e2e..2bff591e8 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -7,7 +7,7 @@ import Foundation @available(iOS 10.0, *) class IterableTaskRunner { - // TODO: Move to `DependencyContainer` after we remove iOS 9 support + // TODO: @tqm Move to `DependencyContainer` after we remove iOS 9 support init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider()) { self.networkSession = networkSession diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index c4acb4bb1..d952f610c 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] diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index c032e6603..8a8967de7 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -81,6 +81,45 @@ class TasksCRUDTests: XCTestCase { 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() @@ -100,10 +139,22 @@ class TasksCRUDTests: XCTestCase { try context.save() } + private let dateProvider = MockDateProvider() + private lazy var persistenceProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider() + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) try! provider.mainQueueContext().deleteAllTasks() try! provider.mainQueueContext().save() return provider }() } + +extension Array where Element: Comparable { + func isAscending() -> Bool { + return zip(self, self.dropFirst()).allSatisfy(<=) + } + + func isDescending() -> Bool { + return zip(self, self.dropFirst()).allSatisfy(>=) + } +} diff --git a/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift b/tests/swift-sdk-swift-tests/IterableNotificationResponseTests.swift index ba2fa905b..2b552adf8 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() From f077833c6627b4218e0f25d0941b02c5183e741e Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 21 Aug 2020 11:04:02 +0530 Subject: [PATCH 33/76] Improved TaskRunner and tests. --- swift-sdk/Internal/ClassExtensions.swift | 10 + .../Internal/IterableNotifications.swift | 61 ++++++ swift-sdk/Internal/IterableTaskRunner.swift | 184 ++++++++++++++---- .../Internal/IterableTaskScheduler.swift | 33 +++- tests/common/CommonMocks.swift | 15 +- .../TaskRunnerTests.swift | 145 ++++++++++---- .../offline-events-tests/TasksCRUDTests.swift | 9 - tests/swift-sdk-swift-tests/InAppTests.swift | 2 +- tests/swift-sdk-swift-tests/InboxTests.swift | 8 +- 9 files changed, 363 insertions(+), 104 deletions(-) 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/IterableNotifications.swift b/swift-sdk/Internal/IterableNotifications.swift index 10db23394..902cafa23 100644 --- a/swift-sdk/Internal/IterableNotifications.swift +++ b/swift-sdk/Internal/IterableNotifications.swift @@ -14,8 +14,69 @@ protocol NotificationCenterProtocol { 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") } +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/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 2bff591e8..0ffe19921 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -6,73 +6,162 @@ import Foundation @available(iOS 10.0, *) -class IterableTaskRunner { +class IterableTaskRunner: NSObject { // TODO: @tqm Move to `DependencyContainer` after we remove iOS 9 support init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), - persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider()) { + persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), + notificationCenter: NotificationCenterProtocol = NotificationCenter.default, + timeInterval: TimeInterval = 1.0 * 60) { + ITBInfo() self.networkSession = networkSession self.persistenceContextProvider = persistenceContextProvider + self.notificationCenter = notificationCenter + self.timeInterval = timeInterval + + super.init() + + self.notificationCenter.addObserver(self, + selector: #selector(onTaskScheduled(notification:)), + name: .iterableTaskScheduled, + object: nil) + } + + func start() { + ITBInfo() + paused = false + run() } - func start() throws { + func stop() { + ITBInfo() + paused = true + timer?.invalidate() + } + + @objc + private func onTaskScheduled(notification: Notification) { + ITBInfo() + if !running && !paused { + timer?.invalidate() + run() + } + } + + private func run() { ITBInfo() - shouldExecute = true persistenceContext.perform { - while self.shouldExecute { - try? self.execute() - Thread.sleep(forTimeInterval: 1.0) + self.processTasks().onSuccess { _ in + ITBInfo("done processing tasks") + self.running = false + self.scheduleNext() } } } - func stop() throws { + private func scheduleNext() { ITBInfo() - shouldExecute = false + if !self.paused { + DispatchQueue.global().async { + ITBInfo("scheduling timer") + let timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: false) { _ in + self.run() + } + self.timer = timer + RunLoop.current.add(timer, forMode: .default) + RunLoop.current.run() + } + } } - func execute() throws { + @discardableResult + private func processTasks() -> Future { ITBInfo() - let tasks = try persistenceContext.findAllTasks() - ITBInfo("numTasks: \(tasks.count)") - for task in tasks { - try execute(task: task).wait() + running = true + + guard !paused else { + ITBInfo("paused") + 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 - func execute(task: IterableTask) throws -> Future { + private func execute(task: IterableTask) -> Future { ITBInfo("executing taskId: \(task.id)") - let result = Promise() + 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) - try processor.process(task: task).onSuccess { taskResult in - switch taskResult { - case let .success(detail: detail): - ITBInfo("task: \(task.id) succeeded") - self.deleteTask(task: task) - if let successDetail = detail as? SendRequestValue { - var userInfo = [AnyHashable: Any]() - userInfo["taskId"] = task.id - userInfo["sendRequestValue"] = successDetail - NotificationCenter.default.post(name: .iterableTaskFinishedWithSuccess, object: self, userInfo: userInfo) - } - case let .failureWithNoRetry(detail: detail): - ITBInfo("task: \(task.id) failed with no retry.") - self.deleteTask(task: task) - if let failureDetail = detail as? SendRequestError { - var userInfo = [AnyHashable: Any]() - userInfo["taskId"] = task.id - userInfo["sendRequestError"] = failureDetail - NotificationCenter.default.post(name: .iterableTaskFinishedWithNoRetry, object: self, userInfo: userInfo) + 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) } - case .failureWithRetry: - ITBInfo("task: \(task.id) processed with retry") - break } - result.resolve(with: ()) + } catch let error { + ITBError("Error proessing task: \(task.id), message: \(error.localizedDescription)") + result.resolve(with: .error) } return result } + deinit { + ITBInfo() + notificationCenter.removeObserver(self) + } + private func deleteTask(task: IterableTask) { do { try persistenceContext.delete(task: task) @@ -81,10 +170,23 @@ class IterableTaskRunner { ITBError(error.localizedDescription) } } - - private var shouldExecute = true + + 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 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 index 09625516e..bd45b8968 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -6,25 +6,40 @@ import Foundation @available(iOS 10.0, *) -struct IterableTaskScheduler { - // TODO: @tqm Use DateProvider +class IterableTaskScheduler { + // TODO: @tqm Move to `DependencyContainer` after we remove iOS 9 support + init(persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), + notificationCenter: NotificationCenterProtocol = NotificationCenter.default, + dateProvider: DateProviderProtocol = SystemDateProvider()) { + self.persistenceContextProvider = persistenceContextProvider + self.notificationCenter = notificationCenter + self.dateProvider = dateProvider + } + func schedule(apiCallRequest: IterableAPICallRequest, - context: IterableTaskContext, - scheduledAt: Date = Date()) throws -> String { - // persist data + context: IterableTaskContext = IterableTaskContext(blocking: true), + scheduledAt: Date? = nil) throws -> String { + ITBInfo() let taskId = IterableUtil.generateUUID() let data = try JSONEncoder().encode(apiCallRequest) - let persistenceContext = persistenceProvider.newBackgroundContext() try persistenceContext.create(task: IterableTask(id: taskId, type: .apiCall, - scheduledAt: scheduledAt, + scheduledAt: scheduledAt ?? dateProvider.currentDate, data: data, - requestedAt: Date())) + requestedAt: dateProvider.currentDate)) try persistenceContext.save() + notificationCenter.post(name: .iterableTaskScheduled, object: self, userInfo: nil) + return taskId } - private let persistenceProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider() + private let persistenceContextProvider: IterablePersistenceContextProvider + private let notificationCenter: NotificationCenterProtocol + private let dateProvider: DateProviderProtocol + + private lazy var persistenceContext: IterablePersistenceContext = { + return persistenceContextProvider.newBackgroundContext() + }() } diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index d952f610c..736256a15 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -360,21 +360,22 @@ class MockNotificationCenter: NotificationCenterProtocol { 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) { + func addCallback(forNotification notification: Notification.Name, callback: @escaping (Notification) -> Void) { 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) } } diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index beecf33be..c244c7886 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -9,32 +9,120 @@ import XCTest class TaskRunnerTests: XCTestCase { override func setUpWithError() throws { - super.setUp() + try super.setUpWithError() - try persistenceContext.deleteAllTasks() - try persistenceContext.save() - - taskExecutor = IterableTaskRunner(networkSession: MockNetworkSession()) - try taskExecutor.start() + IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), + logDelegate: DefaultLogDelegate()) + try! persistenceContextProvider.mainQueueContext().deleteAllTasks() + try! persistenceContextProvider.mainQueueContext().save() } override func tearDownWithError() throws { - try taskExecutor.stop() - Thread.sleep(forTimeInterval: 2.0) + try super.tearDownWithError() } - func testTrackEvent() throws { - IterableLogUtil.sharedInstance = IterableLogUtil(dateProvider: SystemDateProvider(), - logDelegate: DefaultLogDelegate()) + 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(), + 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, + 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, + 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() + } + + 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 { - XCTFail("Could not create trackEvent request") - return + throw IterableError.general(description: "Could not create trackEvent request") } let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, @@ -42,31 +130,22 @@ class TaskRunnerTests: XCTestCase { auth: auth, deviceMetadata: deviceMetadata, iterableRequest: trackEventRequest) - - do { - let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, - context: IterableTaskContext(blocking: true)) - XCTAssertNotNil(taskId) - expectation1.fulfill() - } catch let error { - ITBError(error.localizedDescription) - XCTFail(error.localizedDescription) - } - - Thread.sleep(forTimeInterval: 5.0) - wait(for: [expectation1], timeout: 1000.0) + + return try IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider).schedule(apiCallRequest: apiCallRequest) } private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), platform: JsonValue.iOS.jsonStringValue, appPackageName: Bundle.main.appPackageName ?? "") - private lazy var persistenceContext: IterablePersistenceContext = { - let provider = CoreDataPersistenceContextProvider() - return provider.mainQueueContext() - } () - - private var taskExecutor: IterableTaskRunner! + private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) + return provider + }() + + private let dateProvider = MockDateProvider() } extension TaskRunnerTests: AuthProvider { diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index 8a8967de7..c83afc2d0 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -149,12 +149,3 @@ class TasksCRUDTests: XCTestCase { }() } -extension Array where Element: Comparable { - func isAscending() -> Bool { - return zip(self, self.dropFirst()).allSatisfy(<=) - } - - func isDescending() -> Bool { - return zip(self, self.dropFirst()).allSatisfy(>=) - } -} diff --git a/tests/swift-sdk-swift-tests/InAppTests.swift b/tests/swift-sdk-swift-tests/InAppTests.swift index 3eab0393e..625fec97e 100644 --- a/tests/swift-sdk-swift-tests/InAppTests.swift +++ b/tests/swift-sdk-swift-tests/InAppTests.swift @@ -1205,7 +1205,7 @@ class InAppTests: XCTestCase { """.toJsonDict() let mockNotificationCenter = MockNotificationCenter() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in expectation1.fulfill() } diff --git a/tests/swift-sdk-swift-tests/InboxTests.swift b/tests/swift-sdk-swift-tests/InboxTests.swift index 8672c7a54..0af09d0a7 100644 --- a/tests/swift-sdk-swift-tests/InboxTests.swift +++ b/tests/swift-sdk-swift-tests/InboxTests.swift @@ -291,7 +291,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) @@ -389,7 +389,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) @@ -438,7 +438,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 { @@ -637,7 +637,7 @@ class InboxTests: XCTestCase { XCTAssertEqual(internalAPI.inAppManager.getMessages().count, 1) expectation1.fulfill() - mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { + mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in expectation2.fulfill() } From 60c1d92c582882bfaa691517d1f3f13d885c464a Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 14 Aug 2020 14:44:39 +0530 Subject: [PATCH 34/76] Rename IterableRequestProcessor to RequestProcessorProtocol. --- swift-sdk.xcodeproj/project.pbxproj | 8 ++++---- swift-sdk/Internal/DependencyContainer.swift | 4 ++-- swift-sdk/Internal/DirectCallRequestProcessor.swift | 2 +- swift-sdk/Internal/IterableAPIInternal.swift | 2 +- ...uestProcessor.swift => RequestProcessorProtocol.swift} | 2 +- tests/common/CommonExtensions.swift | 2 +- tests/endpoint-tests/IterableAPISupport.swift | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename swift-sdk/Internal/{IterableRequestProcessor.swift => RequestProcessorProtocol.swift} (99%) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index ec7d1b04e..7dbd38914 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -186,7 +186,7 @@ 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 /* IterableRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF32BDA24E3EA7C0072E2CC /* IterableRequestProcessor.swift */; }; + ACF32BDB24E3EA7C0072E2CC /* RequestProcessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.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 */; }; @@ -504,7 +504,7 @@ 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 /* IterableRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableRequestProcessor.swift; sourceTree = ""; }; + ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProcessorProtocol.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 = ""; }; @@ -841,7 +841,7 @@ AC5E888724E1B7AD00752321 /* Request Processing */ = { isa = PBXGroup; children = ( - ACF32BDA24E3EA7C0072E2CC /* IterableRequestProcessor.swift */, + ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */, AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */, ); name = "Request Processing"; @@ -1574,7 +1574,7 @@ files = ( AC31B042232AB53500BE25EB /* InboxImpressionTracker.swift in Sources */, 55D54656239AE5750093ED1E /* LoggingInternal.swift in Sources */, - ACF32BDB24E3EA7C0072E2CC /* IterableRequestProcessor.swift in Sources */, + ACF32BDB24E3EA7C0072E2CC /* RequestProcessorProtocol.swift in Sources */, AC426CC4211B5497002EDBE8 /* ServerResponse.swift in Sources */, AC219C49225FD7EB00B98631 /* IterableInboxViewController.swift in Sources */, AC3DD9C82142F3650046F886 /* ClassExtensions.swift in Sources */, diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index 2775139c1..dbb89b295 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -20,7 +20,7 @@ protocol DependencyContainerProtocol { var apnsTypeChecker: APNSTypeCheckerProtocol { get } func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol - func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor + func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol } extension DependencyContainerProtocol { @@ -44,7 +44,7 @@ extension DependencyContainerProtocol { } struct DependencyContainer: DependencyContainerProtocol { - func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { DirectCallRequestProcessor(apiClient: apiClient) } diff --git a/swift-sdk/Internal/DirectCallRequestProcessor.swift b/swift-sdk/Internal/DirectCallRequestProcessor.swift index b7c93df5d..6d3d70dee 100644 --- a/swift-sdk/Internal/DirectCallRequestProcessor.swift +++ b/swift-sdk/Internal/DirectCallRequestProcessor.swift @@ -6,7 +6,7 @@ import Foundation /// `IterableAPIinternal` will delegate all network related calls to this struct. -struct DirectCallRequestProcessor: IterableRequestProcessor { +struct DirectCallRequestProcessor: RequestProcessorProtocol { let apiClient: ApiClientProtocol! @discardableResult diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 82f36cee1..18a6f69ff 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -386,7 +386,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deviceMetadata: deviceMetadata) }() - lazy var requestProcessor: IterableRequestProcessor = { + lazy var requestProcessor: RequestProcessorProtocol = { dependencyContainer.createRequestProcessor(apiClient: apiClient) }() diff --git a/swift-sdk/Internal/IterableRequestProcessor.swift b/swift-sdk/Internal/RequestProcessorProtocol.swift similarity index 99% rename from swift-sdk/Internal/IterableRequestProcessor.swift rename to swift-sdk/Internal/RequestProcessorProtocol.swift index 364c8e2ca..31e0f6eb4 100644 --- a/swift-sdk/Internal/IterableRequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessorProtocol.swift @@ -25,7 +25,7 @@ struct UpdateSubscriptionsInfo { } /// `IterableAPIinternal` will delegate all network related calls to this struct. -protocol IterableRequestProcessor { +protocol RequestProcessorProtocol { @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, diff --git a/tests/common/CommonExtensions.swift b/tests/common/CommonExtensions.swift index 1d64a629d..dd401f316 100644 --- a/tests/common/CommonExtensions.swift +++ b/tests/common/CommonExtensions.swift @@ -110,7 +110,7 @@ class MockDependencyContainer: DependencyContainerProtocol { inAppFetcher } - func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { DirectCallRequestProcessor(apiClient: apiClient) } } diff --git a/tests/endpoint-tests/IterableAPISupport.swift b/tests/endpoint-tests/IterableAPISupport.swift index 599a01e4b..1ec0bfdf6 100644 --- a/tests/endpoint-tests/IterableAPISupport.swift +++ b/tests/endpoint-tests/IterableAPISupport.swift @@ -82,7 +82,7 @@ class E2EDependencyContainer: DependencyContainerProtocol { InAppFetcher(apiClient: apiClient) } - func createRequestProcessor(apiClient: ApiClientProtocol) -> IterableRequestProcessor { + func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { DirectCallRequestProcessor(apiClient: apiClient) } } From 05b455a3b18c62b47521377da2d36d615ec47b46 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 24 Aug 2020 13:42:32 +0530 Subject: [PATCH 35/76] RequestProcessorProtocol is implemented by OfflineRequestProcessor and OnlineRequestProcessor. --- swift-sdk.xcodeproj/project.pbxproj | 12 +- swift-sdk/Internal/DependencyContainer.swift | 2 +- .../Internal/OfflineRequestProcessor.swift | 158 ++++++++++++++++++ ...sor.swift => OnlineRequestProcessor.swift} | 38 ++--- tests/common/CommonExtensions.swift | 2 +- 5 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 swift-sdk/Internal/OfflineRequestProcessor.swift rename swift-sdk/Internal/{DirectCallRequestProcessor.swift => OnlineRequestProcessor.swift} (91%) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 7dbd38914..9bc7c29d0 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -80,7 +80,8 @@ 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 */; }; - AC5E888924E1B7CE00752321 /* DirectCallRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */; }; + AC5812F624F3A90F007E6D36 /* OfflineRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */; }; + AC5E888924E1B7CE00752321 /* OnlineRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */; }; AC64626B2140AACF0046E1BD /* IterableAPIResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC64626A2140AACF0046E1BD /* IterableAPIResponseTests.swift */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */; }; @@ -407,7 +408,8 @@ 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 = ""; }; - AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectCallRequestProcessor.swift; sourceTree = ""; }; + AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineRequestProcessor.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 = ""; }; AC684A85222EF75C00F29749 /* InAppMessageParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessageParser.swift; sourceTree = ""; }; AC684A87222F4FDD00F29749 /* InAppDisplayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppDisplayer.swift; sourceTree = ""; }; @@ -842,7 +844,8 @@ isa = PBXGroup; children = ( ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */, - AC5E888824E1B7CE00752321 /* DirectCallRequestProcessor.swift */, + AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */, + AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -1598,6 +1601,7 @@ 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 */, @@ -1638,7 +1642,7 @@ AC31B040232AB42100BE25EB /* InboxSessionManager.swift in Sources */, ACE34AB321376B1000691224 /* UserDefaultsLocalStorage.swift in Sources */, AC8E9268246284F800BEB68E /* DataFieldsHelper.swift in Sources */, - AC5E888924E1B7CE00752321 /* DirectCallRequestProcessor.swift in Sources */, + AC5E888924E1B7CE00752321 /* OnlineRequestProcessor.swift in Sources */, ACC362C324D21332002C67BA /* IterableTaskError.swift in Sources */, ACC362C124D21272002C67BA /* IterableTaskResult.swift in Sources */, AC2C667E20D3111900D46CC9 /* DateProvider.swift in Sources */, diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index dbb89b295..d0796b18f 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -45,7 +45,7 @@ extension DependencyContainerProtocol { struct DependencyContainer: DependencyContainerProtocol { func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { - DirectCallRequestProcessor(apiClient: apiClient) + OnlineRequestProcessor(apiClient: apiClient) } func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol { diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift new file mode 100644 index 000000000..8f5bcd9bf --- /dev/null +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -0,0 +1,158 @@ +// +// Created by Tapash Majumder on 8/24/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +class OfflineRequestProcessor: RequestProcessorProtocol { + @discardableResult + func register(registerTokenInfo: RegisterTokenInfo, + notificationStateProvider: NotificationStateProviderProtocol, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func disableDeviceForCurrentUser(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func disableDeviceForAllUsers(hexToken: String, + withOnSuccess onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func updateUser(_ dataFields: [AnyHashable: Any], + mergeNestedObjects: Bool, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func updateEmail(_ newEmail: String, + withToken _: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackPurchase(_ total: NSNumber, + items: [CommerceItem], + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackPushOpen(_ campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func track(event: String, + dataFields: [AnyHashable: Any]?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func updateSubscriptions(info: UpdateSubscriptionsInfo, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackInAppOpen(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackInAppClick(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackInAppClose(_ message: IterableInAppMessage, + location: InAppLocation, + inboxSessionId: String?, + source: InAppCloseSource?, + clickedUrl: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func track(inboxSession: IterableInboxSession, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func track(inAppDelivery message: IterableInAppMessage, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func inAppConsume(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func inAppConsume(message: IterableInAppMessage, + location: InAppLocation, + source: InAppDeleteSource?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + // MARK: DEPRECATED + + @discardableResult + func trackInAppOpen(_ messageId: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } + + @discardableResult + func trackInAppClick(_ messageId: String, + clickedUrl: String, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + fatalError() + } +} diff --git a/swift-sdk/Internal/DirectCallRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift similarity index 91% rename from swift-sdk/Internal/DirectCallRequestProcessor.swift rename to swift-sdk/Internal/OnlineRequestProcessor.swift index 6d3d70dee..399f9394e 100644 --- a/swift-sdk/Internal/DirectCallRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -6,7 +6,7 @@ import Foundation /// `IterableAPIinternal` will delegate all network related calls to this struct. -struct DirectCallRequestProcessor: RequestProcessorProtocol { +struct OnlineRequestProcessor: RequestProcessorProtocol { let apiClient: ApiClientProtocol! @discardableResult @@ -39,7 +39,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { mergeNestedObjects: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "updateUser", forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) @@ -50,7 +50,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { withToken _: String? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "updateEmail", forResult: apiClient.updateEmail(newEmail: newEmail)) @@ -62,7 +62,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackPurchase", forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) @@ -76,7 +76,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackPushOpen", forResult: apiClient.track(pushOpen: campaignId, @@ -91,7 +91,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackEvent", forResult: apiClient.track(event: event, dataFields: dataFields)) @@ -101,7 +101,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { func updateSubscriptions(info: UpdateSubscriptionsInfo, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "updateSubscriptions", forResult: apiClient.updateSubscriptions(info.emailListIds, @@ -119,7 +119,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppOpen", forResult: result) @@ -134,7 +134,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppClick: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), clickedUrl: clickedUrl) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppClick", forResult: result) @@ -151,7 +151,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { let result = apiClient.track(inAppClose: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), source: source, clickedUrl: clickedUrl) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppClose", forResult: result) @@ -163,7 +163,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inboxSession: inboxSession) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInboxSession", forResult: result) @@ -173,7 +173,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { func track(inAppDelivery message: IterableInAppMessage, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppDelivery", forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) @@ -183,7 +183,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { func inAppConsume(_ messageId: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "inAppConsume", forResult: apiClient.inAppConsume(messageId: messageId)) @@ -197,7 +197,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.inAppConsume(inAppMessageContext: InAppMessageContext.from(message: message, location: location), source: source) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "inAppConsumeWithSource", forResult: result) @@ -210,7 +210,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: messageId) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppOpen", forResult: result) @@ -221,7 +221,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { clickedUrl: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "trackInAppClick", forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) @@ -232,9 +232,9 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { notificationsEnabled: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - let pushServicePlatformString = DirectCallRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) + let pushServicePlatformString = OnlineRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) - return DirectCallRequestProcessor.call(successHandler: onSuccess, + return OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "registerToken", forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, @@ -251,7 +251,7 @@ struct DirectCallRequestProcessor: RequestProcessorProtocol { hexToken: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - DirectCallRequestProcessor.call(successHandler: onSuccess, + OnlineRequestProcessor.call(successHandler: onSuccess, andFailureHandler: onFailure, withIdentifier: "disableDevice", forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) diff --git a/tests/common/CommonExtensions.swift b/tests/common/CommonExtensions.swift index dd401f316..4c92779db 100644 --- a/tests/common/CommonExtensions.swift +++ b/tests/common/CommonExtensions.swift @@ -111,7 +111,7 @@ class MockDependencyContainer: DependencyContainerProtocol { } func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { - DirectCallRequestProcessor(apiClient: apiClient) + OnlineRequestProcessor(apiClient: apiClient) } } From 69fca17cd57ae48fcafb5d93966c1467ae219047 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 24 Aug 2020 23:28:11 +0530 Subject: [PATCH 36/76] First version of RequestProcessor which can switch between offline and online mode. --- swift-sdk.xcodeproj/project.pbxproj | 8 + swift-sdk/Internal/DependencyContainer.swift | 5 - swift-sdk/Internal/IterableAPIInternal.swift | 15 +- .../Internal/OfflineRequestProcessor.swift | 110 +++++++- .../Internal/OnlineRequestProcessor.swift | 22 +- swift-sdk/Internal/RequestProcessor.swift | 242 ++++++++++++++++++ tests/common/CommonExtensions.swift | 4 - tests/endpoint-tests/IterableAPISupport.swift | 4 - .../RequestProcessorTests.swift | 71 +++++ 9 files changed, 460 insertions(+), 21 deletions(-) create mode 100644 swift-sdk/Internal/RequestProcessor.swift create mode 100644 tests/offline-events-tests/RequestProcessorTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 9bc7c29d0..8906192e4 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 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 */; }; AC684A86222EF75C00F29749 /* InAppMessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC684A85222EF75C00F29749 /* InAppMessageParser.swift */; }; @@ -170,6 +171,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 */; }; @@ -409,6 +411,7 @@ 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 = ""; }; AC684A85222EF75C00F29749 /* InAppMessageParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessageParser.swift; sourceTree = ""; }; @@ -487,6 +490,7 @@ 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; }; @@ -846,6 +850,7 @@ ACF32BDA24E3EA7C0072E2CC /* RequestProcessorProtocol.swift */, AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */, AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, + AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -1122,6 +1127,7 @@ ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, + ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1612,6 +1618,7 @@ 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 */, @@ -1798,6 +1805,7 @@ files = ( ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, + ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */, ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */, ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index d0796b18f..5df013e12 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -20,7 +20,6 @@ protocol DependencyContainerProtocol { var apnsTypeChecker: APNSTypeCheckerProtocol { get } func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol - func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol } extension DependencyContainerProtocol { @@ -44,10 +43,6 @@ extension DependencyContainerProtocol { } struct DependencyContainer: DependencyContainerProtocol { - func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { - OnlineRequestProcessor(apiClient: apiClient) - } - func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol { InAppFetcher(apiClient: apiClient) } diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 18a6f69ff..0932e5273 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -387,7 +387,20 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { }() lazy var requestProcessor: RequestProcessorProtocol = { - dependencyContainer.createRequestProcessor(apiClient: apiClient) + if #available(iOS 10.0, *) { + return RequestProcessor(apiKey: apiKey, + authProvider: self, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata, + notificationCenter: dependencyContainer.notificationCenter) + } else { + return OnlineRequestProcessor(apiKey: apiKey, + authProvider: self, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } }() private var deviceAttributes = [String: String]() diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 8f5bcd9bf..0d58fe2a2 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -5,7 +5,20 @@ import Foundation -class OfflineRequestProcessor: RequestProcessorProtocol { +@available(iOS 10.0, *) +struct OfflineRequestProcessor: RequestProcessorProtocol { + init(apiKey: String, + authProvider: AuthProvider, + endPoint: String, + deviceMetadata: DeviceMetadata, + notificationCenter: NotificationCenterProtocol) { + self.apiKey = apiKey + self.authProvider = authProvider + self.endPoint = endPoint + self.deviceMetadata = deviceMetadata + notificationListener = NotificationListener(notificationCenter: notificationCenter) + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, @@ -69,7 +82,30 @@ class OfflineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + ITBInfo() + guard let authProvider = authProvider else { + fatalError("authProvider is missing") + } + + let requestCreator = createRequestCreator(authProvider: authProvider) + guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(event, dataFields: dataFields) else { + return SendRequestError.createErroredFuture(reason: "Could not create trackEvent request") + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: endPoint, + auth: authProvider.auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + + do { + let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, + context: IterableTaskContext(blocking: true)) + return notificationListener.futureFromTask(withTaskId: taskId) + } catch let error { + ITBError(error.localizedDescription) + return SendRequestError.createErroredFuture(reason: error.localizedDescription) + } } @discardableResult @@ -155,4 +191,74 @@ class OfflineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler?) -> Future { fatalError() } + + private let apiKey: String + private weak var authProvider: AuthProvider? + private let endPoint: String + private let deviceMetadata: DeviceMetadata + private let notificationListener: NotificationListener + + private func createRequestCreator(authProvider: AuthProvider) -> RequestCreator { + return RequestCreator(apiKey: apiKey, auth: authProvider.auth, deviceMetadata: deviceMetadata) + } + + private class NotificationListener: NSObject { + init(notificationCenter: NotificationCenterProtocol) { + self.notificationCenter = notificationCenter + super.init() + self.notificationCenter.addObserver(self, + selector: #selector(on(notification:)), + name: .iterableTaskFinishedWithSuccess, object: nil) + } + + deinit { + self.notificationCenter.removeObserver(self) + } + + func futureFromTask(withTaskId taskId: String) -> Future { + ITBInfo() + let result = Promise() + pendingTasksMap[taskId] = result + return result + } + + @objc + private func on(notification: Notification) { + switch notification.name { + case .iterableTaskFinishedWithSuccess: + 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") + } + case .iterableTaskFinishedWithNoRetry: + 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") + } + case .iterableTaskFinishedWithRetry: + break + default: + break + } + } + + private var notificationCenter: NotificationCenterProtocol + private var pendingTasksMap = [String: Promise]() + } } diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index 399f9394e..d85c36841 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -7,8 +7,18 @@ import Foundation /// `IterableAPIinternal` will delegate all network related calls to this struct. struct OnlineRequestProcessor: RequestProcessorProtocol { - let apiClient: ApiClientProtocol! - + init(apiKey: String, + authProvider: AuthProvider, + endPoint: String, + networkSession: NetworkSessionProtocol, + deviceMetadata: DeviceMetadata) { + apiClient = ApiClient(apiKey: apiKey, + authProvider: authProvider, + endPoint: endPoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, @@ -40,9 +50,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateUser", - forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + andFailureHandler: onFailure, + withIdentifier: "updateUser", + forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) } @discardableResult @@ -227,6 +237,8 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) } + private let apiClient: ApiClientProtocol + @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift new file mode 100644 index 000000000..9bd3dd898 --- /dev/null +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -0,0 +1,242 @@ +// +// Created by Tapash Majumder on 8/24/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import Foundation + +@available(iOS 10.0, *) +struct RequestProcessor: RequestProcessorProtocol { + init(apiKey: String, + authProvider: AuthProvider, + endPoint: String, + networkSession: NetworkSessionProtocol, + deviceMetadata: DeviceMetadata, + notificationCenter: NotificationCenterProtocol) { + offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + endPoint: endPoint, + deviceMetadata: deviceMetadata, + notificationCenter: notificationCenter) + onlineProcessor = OnlineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + endPoint: endPoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } + + @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, + withToken token: String?, + onSuccess: OnSuccessHandler?, + onFailure: OnFailureHandler?) -> Future { + chooseRequestProcessor().updateEmail(newEmail, + withToken: token, + 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 offlineProcessor: OfflineRequestProcessor + private let onlineProcessor: OnlineRequestProcessor + + private func chooseRequestProcessor() -> RequestProcessorProtocol { + onlineProcessor + } +} diff --git a/tests/common/CommonExtensions.swift b/tests/common/CommonExtensions.swift index 4c92779db..fb3bf17e3 100644 --- a/tests/common/CommonExtensions.swift +++ b/tests/common/CommonExtensions.swift @@ -109,10 +109,6 @@ class MockDependencyContainer: DependencyContainerProtocol { func createInAppFetcher(apiClient _: ApiClientProtocol) -> InAppFetcherProtocol { inAppFetcher } - - func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { - OnlineRequestProcessor(apiClient: apiClient) - } } extension IterableAPI { diff --git a/tests/endpoint-tests/IterableAPISupport.swift b/tests/endpoint-tests/IterableAPISupport.swift index 1ec0bfdf6..8798a9653 100644 --- a/tests/endpoint-tests/IterableAPISupport.swift +++ b/tests/endpoint-tests/IterableAPISupport.swift @@ -81,10 +81,6 @@ class E2EDependencyContainer: DependencyContainerProtocol { func createInAppFetcher(apiClient: ApiClientProtocol) -> InAppFetcherProtocol { InAppFetcher(apiClient: apiClient) } - - func createRequestProcessor(apiClient: ApiClientProtocol) -> RequestProcessorProtocol { - DirectCallRequestProcessor(apiClient: apiClient) - } } extension IterableAPIInternal { diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift new file mode 100644 index 000000000..f3d6a5d84 --- /dev/null +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -0,0 +1,71 @@ +// +// 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 testTrackEvent() throws { + let expectation1 = expectation(description: #function) + let apiKey = "zee-api-key" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let notificationCenter = MockNotificationCenter() + + let requestProcessor = OfflineRequestProcessor(apiKey: apiKey, + authProvider: self, + endPoint: Endpoint.api, + deviceMetadata: deviceMetadata, + notificationCenter: notificationCenter) + requestProcessor.track(event: eventName, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + .onSuccess { json in + expectation1.fulfill() + }.onError { error in + XCTFail() + } + + let taskRunner = IterableTaskRunner(networkSession: MockNetworkSession(), + notificationCenter: notificationCenter, + timeInterval: 0.5) + taskRunner.start() + wait(for: [expectation1], timeout: 15.0) + taskRunner.stop() + } + + + private 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) + } +} From 007ae812cfc02ac0470ce92db5f213e2c3b53b5d Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 25 Aug 2020 13:18:29 +0530 Subject: [PATCH 37/76] Rename to OfflineRequestProcessor. --- swift-sdk.xcodeproj/project.pbxproj | 8 ++++---- ...ssorTests.swift => OfflineRequestProcessorTests.swift} | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename tests/offline-events-tests/{RequestProcessorTests.swift => OfflineRequestProcessorTests.swift} (96%) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 8906192e4..074395987 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -171,7 +171,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 */; }; + ACCF274C24F40C85004862D5 /* OfflineRequestProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACCF274B24F40C85004862D5 /* OfflineRequestProcessorTests.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 */; }; @@ -490,7 +490,7 @@ 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 = ""; }; + ACCF274B24F40C85004862D5 /* OfflineRequestProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineRequestProcessorTests.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; }; @@ -1127,7 +1127,7 @@ ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, - ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, + ACCF274B24F40C85004862D5 /* OfflineRequestProcessorTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1805,7 +1805,7 @@ files = ( ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, - ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */, + ACCF274C24F40C85004862D5 /* OfflineRequestProcessorTests.swift in Sources */, ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */, ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift similarity index 96% rename from tests/offline-events-tests/RequestProcessorTests.swift rename to tests/offline-events-tests/OfflineRequestProcessorTests.swift index f3d6a5d84..ac4a6a855 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import IterableSDK -class RequestProcessorTests: XCTestCase { +class OfflineRequestProcessorTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() @@ -64,7 +64,7 @@ class RequestProcessorTests: XCTestCase { }() } -extension RequestProcessorTests: AuthProvider { +extension OfflineRequestProcessorTests: AuthProvider { var auth: Auth { Auth(userId: nil, email: "user@example.com", authToken: nil) } From 613bd607b79dae9c3311f1ef493adebd6effa9c2 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 25 Aug 2020 14:58:50 +0530 Subject: [PATCH 38/76] Handle error for offline mode. --- .../Internal/OfflineRequestProcessor.swift | 63 ++++++++++--------- .../OfflineRequestProcessorTests.swift | 33 +++++++++- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 0d58fe2a2..9e5b12391 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -207,8 +207,11 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { self.notificationCenter = notificationCenter super.init() self.notificationCenter.addObserver(self, - selector: #selector(on(notification:)), + selector: #selector(onTaskFinishedWithSuccess(notification:)), name: .iterableTaskFinishedWithSuccess, object: nil) + self.notificationCenter.addObserver(self, + selector: #selector(onTaskFinishedWithNoRetry(notification:)), + name: .iterableTaskFinishedWithNoRetry, object: nil) } deinit { @@ -221,43 +224,41 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { pendingTasksMap[taskId] = result return result } - + @objc - private func on(notification: Notification) { - switch notification.name { - case .iterableTaskFinishedWithSuccess: - 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)") - } + 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 taskId for notification") + ITBError("could not find promise for taskId: \(taskId)") } - case .iterableTaskFinishedWithNoRetry: - 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") + } + } + + @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 taskId for notification") + ITBError("could not find promise for taskId: \(taskId)") } - case .iterableTaskFinishedWithRetry: - break - default: - break + } else { + ITBError("Could not find taskId for notification") } } - + private var notificationCenter: NotificationCenterProtocol private var pendingTasksMap = [String: Promise]() } diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index ac4a6a855..0a541b3cc 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -52,7 +52,38 @@ class OfflineRequestProcessorTests: XCTestCase { taskRunner.stop() } - + func testTrackEventWithNoRetry() throws { + let expectation1 = expectation(description: #function) + let apiKey = "zee-api-key" + let eventName = "CustomEvent1" + let dataFields = ["var1": "val1", "var2": "val2"] + + let notificationCenter = MockNotificationCenter() + + let requestProcessor = OfflineRequestProcessor(apiKey: apiKey, + authProvider: self, + endPoint: Endpoint.api, + deviceMetadata: deviceMetadata, + notificationCenter: notificationCenter) + requestProcessor.track(event: eventName, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + .onSuccess { json in + XCTFail() + }.onError { error in + expectation1.fulfill() + } + + let networkSession = MockNetworkSession(statusCode: 400) + let taskRunner = IterableTaskRunner(networkSession: networkSession, + notificationCenter: notificationCenter, + timeInterval: 0.5) + taskRunner.start() + wait(for: [expectation1], timeout: 15.0) + taskRunner.stop() + } + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), platform: JsonValue.iOS.jsonStringValue, appPackageName: Bundle.main.appPackageName ?? "") From 0beb8203c84a3bab3f98954c18c8c1906bb99f13 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 25 Aug 2020 21:45:44 +0530 Subject: [PATCH 39/76] Refactor test for easy reuse. --- .../OfflineRequestProcessorTests.swift | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index 0a541b3cc..49d2e5b2b 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -20,62 +20,83 @@ class OfflineRequestProcessorTests: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() } - + func testTrackEvent() throws { - let expectation1 = expectation(description: #function) - let apiKey = "zee-api-key" + let notificationCenter = MockNotificationCenter() let eventName = "CustomEvent1" let dataFields = ["var1": "val1", "var2": "val2"] - - let notificationCenter = MockNotificationCenter() - - let requestProcessor = OfflineRequestProcessor(apiKey: apiKey, - authProvider: self, - endPoint: Endpoint.api, - deviceMetadata: deviceMetadata, - notificationCenter: notificationCenter) - requestProcessor.track(event: eventName, - dataFields: dataFields, - onSuccess: nil, - onFailure: nil) - .onSuccess { json in + let bodyDict: [String: Any] = [ + "eventName": eventName, + "dataFields": dataFields, + "email": "user@example.com" + ] + let requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) + let request: () -> Future = { + requestProcessor.track(event: eventName, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + } + testProcessRequestWithSuccess(notificationCenter: notificationCenter, + path: Const.Path.trackEvent, + bodyDict: bodyDict, + request: request) + testProcessRequestWithFailure(notificationCenter: notificationCenter, + path: Const.Path.trackEvent, + bodyDict: bodyDict, + request: request) + } + + private func createRequestProcessor(notificationCenter: NotificationCenterProtocol) -> RequestProcessorProtocol { + OfflineRequestProcessor(apiKey: "zee-api-key", + authProvider: self, + endPoint: Endpoint.api, + deviceMetadata: deviceMetadata, + notificationCenter: notificationCenter) + } + + private func testProcessRequestWithSuccess(notificationCenter: NotificationCenterProtocol, + path: String, + bodyDict: [AnyHashable: Any], + request: () -> Future) { + let expectation1 = expectation(description: #function) + let networkSession = MockNetworkSession() + + request().onSuccess { json in expectation1.fulfill() }.onError { error in XCTFail() } - let taskRunner = IterableTaskRunner(networkSession: MockNetworkSession(), + networkSession.requestCallback = { request in + TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) + XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) + } + let taskRunner = IterableTaskRunner(networkSession: networkSession, notificationCenter: notificationCenter, timeInterval: 0.5) taskRunner.start() wait(for: [expectation1], timeout: 15.0) taskRunner.stop() } - - func testTrackEventWithNoRetry() throws { + + private func testProcessRequestWithFailure(notificationCenter: NotificationCenterProtocol, + path: String, + bodyDict: [AnyHashable: Any], + request: () -> Future) { let expectation1 = expectation(description: #function) - let apiKey = "zee-api-key" - let eventName = "CustomEvent1" - let dataFields = ["var1": "val1", "var2": "val2"] - - let notificationCenter = MockNotificationCenter() - - let requestProcessor = OfflineRequestProcessor(apiKey: apiKey, - authProvider: self, - endPoint: Endpoint.api, - deviceMetadata: deviceMetadata, - notificationCenter: notificationCenter) - requestProcessor.track(event: eventName, - dataFields: dataFields, - onSuccess: nil, - onFailure: nil) - .onSuccess { json in + let networkSession = MockNetworkSession(statusCode: 400) + + request().onSuccess { json in XCTFail() }.onError { error in expectation1.fulfill() } - 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 taskRunner = IterableTaskRunner(networkSession: networkSession, notificationCenter: notificationCenter, timeInterval: 0.5) @@ -83,7 +104,7 @@ class OfflineRequestProcessorTests: XCTestCase { wait(for: [expectation1], timeout: 15.0) taskRunner.stop() } - + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), platform: JsonValue.iOS.jsonStringValue, appPackageName: Bundle.main.appPackageName ?? "") From 5f6c3c5ef317903869887f03fa4b7728c5aabe3f Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 11:00:55 +0530 Subject: [PATCH 40/76] 1. Fix API client RegisterTokenInfo. 2. Add registerDevice to OfflineRequestProcessor. --- swift-sdk.xcodeproj/project.pbxproj | 2 + swift-sdk/Internal/ApiClient.swift | 15 +----- swift-sdk/Internal/ApiClientProtocol.swift | 8 +-- .../Internal/OfflineRequestProcessor.swift | 27 +++++++++- .../Internal/OnlineRequestProcessor.swift | 26 ++-------- swift-sdk/Internal/RequestCreator.swift | 31 ++++++----- .../OfflineRequestProcessorTests.swift | 52 +++++++++++++++++++ .../InAppHelperTests.swift | 7 +-- 8 files changed, 107 insertions(+), 61 deletions(-) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 074395987..e8f4c2319 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 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 */; }; @@ -1808,6 +1809,7 @@ ACCF274C24F40C85004862D5 /* OfflineRequestProcessorTests.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 */, diff --git a/swift-sdk/Internal/ApiClient.swift b/swift-sdk/Internal/ApiClient.swift index 74254f010..0674f7e7a 100644 --- a/swift-sdk/Internal/ApiClient.swift +++ b/swift-sdk/Internal/ApiClient.swift @@ -85,19 +85,8 @@ 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, + func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Future { + send(iterableRequestResult: createRequestCreator().createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, notificationsEnabled: notificationsEnabled)) } 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/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 9e5b12391..9ebad9a5b 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -24,7 +24,32 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { notificationStateProvider: NotificationStateProviderProtocol, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + guard let authProvider = authProvider else { + fatalError("authProvider is missing") + } + + let requestCreator = createRequestCreator(authProvider: authProvider) + guard case let Result.success(trackEventRequest) = + requestCreator.createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, + notificationsEnabled: true) + else { + return SendRequestError.createErroredFuture(reason: "Could not create trackEvent request") + } + + let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, + endPoint: endPoint, + auth: authProvider.auth, + deviceMetadata: deviceMetadata, + iterableRequest: trackEventRequest) + + do { + let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, + context: IterableTaskContext(blocking: true)) + return notificationListener.futureFromTask(withTaskId: taskId) + } catch let error { + ITBError(error.localizedDescription) + return SendRequestError.createErroredFuture(reason: error.localizedDescription) + } } @discardableResult diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index d85c36841..079a30483 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -244,18 +244,11 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { notificationsEnabled: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - let pushServicePlatformString = OnlineRequestProcessor.pushServicePlatformToString(registerTokenInfo.pushServicePlatform, apnsType: registerTokenInfo.apnsType) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "registerToken", - forResult: apiClient.register(hexToken: registerTokenInfo.hexToken, - appName: registerTokenInfo.appName, - deviceId: registerTokenInfo.deviceId, - sdkVersion: registerTokenInfo.sdkVersion, - deviceAttributes: registerTokenInfo.deviceAttributes, - pushServicePlatform: pushServicePlatformString, - notificationsEnabled: notificationsEnabled)) + andFailureHandler: onFailure, + withIdentifier: "registerToken", + forResult: apiClient.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: notificationsEnabled)) } @discardableResult @@ -269,17 +262,6 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) } - 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 - } - } - @discardableResult private static func call(successHandler onSuccess: OnSuccessHandler? = nil, andFailureHandler onFailure: OnFailureHandler? = nil, diff --git a/swift-sdk/Internal/RequestCreator.swift b/swift-sdk/Internal/RequestCreator.swift index 7335010dd..5b1f24e13 100644 --- a/swift-sdk/Internal/RequestCreator.swift +++ b/swift-sdk/Internal/RequestCreator.swift @@ -31,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, ] @@ -444,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/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index 49d2e5b2b..d10705cd7 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -21,6 +21,58 @@ class OfflineRequestProcessorTests: XCTestCase { try super.tearDownWithError() } + func testRegister() throws { + let notificationCenter = MockNotificationCenter() + 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 requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) + let request: () -> Future = { + requestProcessor.register(registerTokenInfo: registerTokenInfo, + notificationStateProvider: MockNotificationStateProvider(enabled: true), + onSuccess: nil, + onFailure: nil) + } + testProcessRequestWithSuccess(notificationCenter: notificationCenter, + path: Const.Path.registerDeviceToken, + bodyDict: bodyDict, + request: request) + testProcessRequestWithFailure(notificationCenter: notificationCenter, + path: Const.Path.registerDeviceToken, + bodyDict: bodyDict, + request: request) + } + func testTrackEvent() throws { let notificationCenter = MockNotificationCenter() let eventName = "CustomEvent1" diff --git a/tests/swift-sdk-swift-tests/InAppHelperTests.swift b/tests/swift-sdk-swift-tests/InAppHelperTests.swift index 857481569..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() } From c45c22f5d2faaf0482cded8a845b15b31a47f48f Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 13:20:44 +0530 Subject: [PATCH 41/76] More refactoring of test. --- .../OfflineRequestProcessorTests.swift | 98 ++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index d10705cd7..1c079d775 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -22,7 +22,6 @@ class OfflineRequestProcessorTests: XCTestCase { } func testRegister() throws { - let notificationCenter = MockNotificationCenter() let registerTokenInfo = RegisterTokenInfo(hexToken: "zee-token", appName: "zee-app-name", pushServicePlatform: .auto, @@ -56,25 +55,20 @@ class OfflineRequestProcessorTests: XCTestCase { "device": deviceDict, "email": "user@example.com" ] - let requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) - let request: () -> Future = { + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.register(registerTokenInfo: registerTokenInfo, notificationStateProvider: MockNotificationStateProvider(enabled: true), onSuccess: nil, onFailure: nil) } - testProcessRequestWithSuccess(notificationCenter: notificationCenter, - path: Const.Path.registerDeviceToken, - bodyDict: bodyDict, - request: request) - testProcessRequestWithFailure(notificationCenter: notificationCenter, - path: Const.Path.registerDeviceToken, - bodyDict: bodyDict, - request: request) + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.registerDeviceToken, + bodyDict: bodyDict) } - + func testTrackEvent() throws { - let notificationCenter = MockNotificationCenter() let eventName = "CustomEvent1" let dataFields = ["var1": "val1", "var2": "val2"] let bodyDict: [String: Any] = [ @@ -82,21 +76,33 @@ class OfflineRequestProcessorTests: XCTestCase { "dataFields": dataFields, "email": "user@example.com" ] - let requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) - let request: () -> Future = { + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.track(event: eventName, dataFields: dataFields, onSuccess: nil, onFailure: nil) } - testProcessRequestWithSuccess(notificationCenter: notificationCenter, - path: Const.Path.trackEvent, - bodyDict: bodyDict, - request: request) - testProcessRequestWithFailure(notificationCenter: notificationCenter, - path: Const.Path.trackEvent, - bodyDict: bodyDict, - request: request) + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackEvent, + bodyDict: bodyDict) + } + + private func processRequestWithSuccessAndFailure(requestGenerator: (RequestProcessorProtocol) -> Future, + path: String, + bodyDict: [AnyHashable: Any]) throws { + let notificationCenter = MockNotificationCenter() + let requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) + let request = { requestGenerator(requestProcessor) } + processRequestWithSuccess(notificationCenter: notificationCenter, + path: path, + bodyDict: bodyDict, + request: request) + processRequestWithFailure(notificationCenter: notificationCenter, + path: path, + bodyDict: bodyDict, + request: request) } private func createRequestProcessor(notificationCenter: NotificationCenterProtocol) -> RequestProcessorProtocol { @@ -107,12 +113,11 @@ class OfflineRequestProcessorTests: XCTestCase { notificationCenter: notificationCenter) } - private func testProcessRequestWithSuccess(notificationCenter: NotificationCenterProtocol, - path: String, - bodyDict: [AnyHashable: Any], - request: () -> Future) { + private func processRequestWithSuccess(notificationCenter: NotificationCenterProtocol, + path: String, + bodyDict: [AnyHashable: Any], + request: () -> Future) { let expectation1 = expectation(description: #function) - let networkSession = MockNetworkSession() request().onSuccess { json in expectation1.fulfill() @@ -120,24 +125,18 @@ class OfflineRequestProcessorTests: XCTestCase { XCTFail() } - networkSession.requestCallback = { request in - TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) - XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) - } - let taskRunner = IterableTaskRunner(networkSession: networkSession, - notificationCenter: notificationCenter, - timeInterval: 0.5) - taskRunner.start() - wait(for: [expectation1], timeout: 15.0) - taskRunner.stop() + validateRequest(networkSession: MockNetworkSession(), + path: path, + bodyDict: bodyDict, + notificationCenter: notificationCenter, + expectation: expectation1) } - private func testProcessRequestWithFailure(notificationCenter: NotificationCenterProtocol, - path: String, - bodyDict: [AnyHashable: Any], - request: () -> Future) { + private func processRequestWithFailure(notificationCenter: NotificationCenterProtocol, + path: String, + bodyDict: [AnyHashable: Any], + request: () -> Future) { let expectation1 = expectation(description: #function) - let networkSession = MockNetworkSession(statusCode: 400) request().onSuccess { json in XCTFail() @@ -145,6 +144,17 @@ class OfflineRequestProcessorTests: XCTestCase { expectation1.fulfill() } + validateRequest(networkSession: MockNetworkSession(statusCode: 400), + path: path, + bodyDict: bodyDict, + notificationCenter: notificationCenter, + expectation: expectation1) + } + + private func validateRequest(networkSession: MockNetworkSession, + path: String, bodyDict: [AnyHashable : Any], + notificationCenter: NotificationCenterProtocol, + expectation: XCTestExpectation) { networkSession.requestCallback = { request in TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) @@ -153,7 +163,7 @@ class OfflineRequestProcessorTests: XCTestCase { notificationCenter: notificationCenter, timeInterval: 0.5) taskRunner.start() - wait(for: [expectation1], timeout: 15.0) + wait(for: [expectation], timeout: 15.0) taskRunner.stop() } From 55a8db34afa0484d0fe120c7f9149851e6150e89 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 14:32:25 +0530 Subject: [PATCH 42/76] Convert disableDeviceForCurrentUser. Refactor OfflineRequestProcessor to extract common functionality. --- swift-sdk.xcodeproj/project.pbxproj | 4 + .../Internal/OfflineRequestProcessor.swift | 109 +++++++------- .../Internal/OnlineRequestProcessor.swift | 141 +++++++++--------- swift-sdk/Internal/RequestProcessorUtil.swift | 52 +++++++ .../OfflineRequestProcessorTests.swift | 20 ++- 5 files changed, 205 insertions(+), 121 deletions(-) create mode 100644 swift-sdk/Internal/RequestProcessorUtil.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index e8f4c2319..dd6684fdb 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ 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 */; }; AC4095A422B18B9D006EF67C /* InboxViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4095A322B18B9D006EF67C /* InboxViewControllerViewModel.swift */; }; @@ -399,6 +400,7 @@ 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 = ""; }; AC4095A322B18B9D006EF67C /* InboxViewControllerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewControllerViewModel.swift; sourceTree = ""; }; @@ -852,6 +854,7 @@ AC5E888824E1B7CE00752321 /* OnlineRequestProcessor.swift */, AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */, + AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -1658,6 +1661,7 @@ 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 */, diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 9ebad9a5b..cd82f77eb 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -22,41 +22,31 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, - onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) -> Future { - guard let authProvider = authProvider else { - fatalError("authProvider is missing") - } - - let requestCreator = createRequestCreator(authProvider: authProvider) - guard case let Result.success(trackEventRequest) = + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { + let requestGenerator = { (requestCreator: RequestCreator) in requestCreator.createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, notificationsEnabled: true) - else { - return SendRequestError.createErroredFuture(reason: "Could not create trackEvent request") } - let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, - endPoint: endPoint, - auth: authProvider.auth, - deviceMetadata: deviceMetadata, - iterableRequest: trackEventRequest) - - do { - let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, - context: IterableTaskContext(blocking: true)) - return notificationListener.futureFromTask(withTaskId: taskId) - } catch let error { - ITBError(error.localizedDescription) - return SendRequestError.createErroredFuture(reason: error.localizedDescription) - } + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult func disableDeviceForCurrentUser(hexToken: String, - withOnSuccess onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 @@ -105,32 +95,18 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { @discardableResult func track(event: String, dataFields: [AnyHashable: Any]?, - onSuccess: OnSuccessHandler?, - onFailure: OnFailureHandler?) -> Future { + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Future { ITBInfo() - guard let authProvider = authProvider else { - fatalError("authProvider is missing") - } - - let requestCreator = createRequestCreator(authProvider: authProvider) - guard case let Result.success(trackEventRequest) = requestCreator.createTrackEventRequest(event, dataFields: dataFields) else { - return SendRequestError.createErroredFuture(reason: "Could not create trackEvent request") + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackEventRequest(event, + dataFields: dataFields) } - - let apiCallRequest = IterableAPICallRequest(apiKey: apiKey, - endPoint: endPoint, - auth: authProvider.auth, - deviceMetadata: deviceMetadata, - iterableRequest: trackEventRequest) - do { - let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, - context: IterableTaskContext(blocking: true)) - return notificationListener.futureFromTask(withTaskId: taskId) - } catch let error { - ITBError(error.localizedDescription) - return SendRequestError.createErroredFuture(reason: error.localizedDescription) - } + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult @@ -227,6 +203,39 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { 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) + + do { + let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, + context: IterableTaskContext(blocking: true)) + let result = notificationListener.futureFromTask(withTaskId: taskId) + return RequestProcessorUtil.apply(successHandler: onSuccess, + andFailureHandler: onFailure, + toResult: result, + withIdentifier: identifier) + } catch let error { + ITBError(error.localizedDescription) + return SendRequestError.createErroredFuture(reason: error.localizedDescription) + } + } + private class NotificationListener: NSObject { init(notificationCenter: NotificationCenterProtocol) { self.notificationCenter = notificationCenter diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index 079a30483..cebdf43a4 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -18,7 +18,7 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { networkSession: networkSession, deviceMetadata: deviceMetadata) } - + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, @@ -61,9 +61,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateEmail", - forResult: apiClient.updateEmail(newEmail: newEmail)) + andFailureHandler: onFailure, + withIdentifier: "updateEmail", + forResult: apiClient.updateEmail(newEmail: newEmail)) } @discardableResult @@ -73,9 +73,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPurchase", - forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) + andFailureHandler: onFailure, + withIdentifier: "trackPurchase", + forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) } @discardableResult @@ -87,13 +87,13 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPushOpen", - forResult: apiClient.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) + andFailureHandler: onFailure, + withIdentifier: "trackPushOpen", + forResult: apiClient.track(pushOpen: campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields)) } @discardableResult @@ -102,9 +102,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackEvent", - forResult: apiClient.track(event: event, dataFields: dataFields)) + andFailureHandler: onFailure, + withIdentifier: "trackEvent", + forResult: apiClient.track(event: event, dataFields: dataFields)) } @discardableResult @@ -112,14 +112,14 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(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)) + 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 @@ -130,9 +130,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) } @discardableResult @@ -145,9 +145,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { let result = apiClient.track(inAppClick: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), clickedUrl: clickedUrl) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: result) } @discardableResult @@ -162,9 +162,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { source: source, clickedUrl: clickedUrl) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClose", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInAppClose", + forResult: result) } @discardableResult @@ -174,9 +174,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { let result = apiClient.track(inboxSession: inboxSession) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInboxSession", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInboxSession", + forResult: result) } @discardableResult @@ -184,9 +184,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppDelivery", - forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) + andFailureHandler: onFailure, + withIdentifier: "trackInAppDelivery", + forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) } @discardableResult @@ -194,9 +194,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsume", - forResult: apiClient.inAppConsume(messageId: messageId)) + andFailureHandler: onFailure, + withIdentifier: "inAppConsume", + forResult: apiClient.inAppConsume(messageId: messageId)) } @discardableResult @@ -208,9 +208,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { let result = apiClient.inAppConsume(inAppMessageContext: InAppMessageContext.from(message: message, location: location), source: source) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsumeWithSource", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "inAppConsumeWithSource", + forResult: result) } // MARK: DEPRECATED @@ -221,9 +221,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: messageId) return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) } @discardableResult @@ -232,9 +232,9 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) } private let apiClient: ApiClientProtocol @@ -257,11 +257,12 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "disableDevice", - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) } + // TODO: @tqm, replace with RequestProcessorUtil.apply(...) @discardableResult private static func call(successHandler onSuccess: OnSuccessHandler? = nil, andFailureHandler onFailure: OnFailureHandler? = nil, @@ -284,25 +285,25 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { } static func defaultOnSuccess(_ identifier: String) -> OnSuccessHandler { - { data in - if let data = data { - ITBInfo("\(identifier) succeeded, got response: \(data)") - } else { - ITBInfo("\(identifier) succeeded.") - } + { 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) + { 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/Internal/RequestProcessorUtil.swift b/swift-sdk/Internal/RequestProcessorUtil.swift new file mode 100644 index 000000000..fbe30e524 --- /dev/null +++ b/swift-sdk/Internal/RequestProcessorUtil.swift @@ -0,0 +1,52 @@ +// +// 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, + 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 let onFailure = onFailure { + onFailure(error.reason, error.data) + } else { + defaultOnFailure(identifier)(error.reason, error.data) + } + } + return result + } + + 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) + } + } +} diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index 1c079d775..11803ee43 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -67,7 +67,25 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.registerDeviceToken, bodyDict: bodyDict) } - + + func testDisableUserforCurrentUser() throws { + let hexToken = "zee-token" + let bodyDict: [String: Any] = [ + "token": hexToken, + "email": "user@example.com" + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.disableDeviceForCurrentUser(hexToken: hexToken, + withOnSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.disableDevice, + bodyDict: bodyDict) + } + func testTrackEvent() throws { let eventName = "CustomEvent1" let dataFields = ["var1": "val1", "var2": "val2"] From e70cccfb1796dd769414df88c6a315d72e02d72d Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 16:46:55 +0530 Subject: [PATCH 43/76] Add disableDeviceForAllUsers, updateUser, updateEmail to OfflineRequestProcessor. --- .../Internal/OfflineRequestProcessor.swift | 27 ++++++++- .../OfflineRequestProcessorTests.swift | 57 ++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index cd82f77eb..cafd07a35 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -53,7 +53,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { func disableDeviceForAllUsers(hexToken: String, withOnSuccess onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createDisableDeviceRequest(forAllUsers: true, hexToken: hexToken) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult @@ -61,7 +68,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { mergeNestedObjects: Bool, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createUpdateUserRequest(dataFields: dataFields, mergeNestedObjects: mergeNestedObjects) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult @@ -69,7 +83,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { withToken _: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createUpdateEmailRequest(newEmail: newEmail) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index 11803ee43..4b0975267 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -86,6 +86,23 @@ class OfflineRequestProcessorTests: XCTestCase { bodyDict: bodyDict) } + func testDisableUserforAllUsers() throws { + let hexToken = "zee-token" + let bodyDict: [String: Any] = [ + "token": hexToken, + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.disableDeviceForAllUsers(hexToken: hexToken, + withOnSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.disableDevice, + bodyDict: bodyDict) + } + func testTrackEvent() throws { let eventName = "CustomEvent1" let dataFields = ["var1": "val1", "var2": "val2"] @@ -106,7 +123,45 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.trackEvent, bodyDict: bodyDict) } - + + func testUpdateUser() throws { + let dataFields = ["var1": "val1", "var2": "val2"] + let bodyDict: [String: Any] = [ + "dataFields": dataFields, + "email": "user@example.com", + "mergeNestedObjects": true + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateUser(dataFields, + mergeNestedObjects: true, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateUser, + bodyDict: bodyDict) + } + + func testUpdateEmail() throws { + let bodyDict: [String: Any] = [ + "currentEmail": "user@example.com", + "newEmail": "new_user@example.com" + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateEmail("new_user@example.com", + withToken: nil, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateEmail, + bodyDict: bodyDict) + } + private func processRequestWithSuccessAndFailure(requestGenerator: (RequestProcessorProtocol) -> Future, path: String, bodyDict: [AnyHashable: Any]) throws { From 400f9d84b4894afe275b5af670f1518fe369aa0e Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 17:00:16 +0530 Subject: [PATCH 44/76] No need to pass token to request processor. --- swift-sdk/Internal/IterableAPIInternal.swift | 2 +- swift-sdk/Internal/OfflineRequestProcessor.swift | 1 - swift-sdk/Internal/OnlineRequestProcessor.swift | 1 - swift-sdk/Internal/RequestProcessor.swift | 2 -- swift-sdk/Internal/RequestProcessorProtocol.swift | 1 - tests/offline-events-tests/OfflineRequestProcessorTests.swift | 1 - 6 files changed, 1 insertion(+), 7 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 0932e5273..f71481de5 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -195,7 +195,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { withToken token: String? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - requestProcessor.updateEmail(newEmail, withToken: token, onSuccess: nil, onFailure: nil).onSuccess { json in + requestProcessor.updateEmail(newEmail, onSuccess: nil, onFailure: nil).onSuccess { json in if self.email != nil { self.setEmail(newEmail, withToken: token) } diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index cafd07a35..8e516718d 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -80,7 +80,6 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { @discardableResult func updateEmail(_ newEmail: String, - withToken _: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { let requestGenerator = { (requestCreator: RequestCreator) in diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index cebdf43a4..0dc65b8b6 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -57,7 +57,6 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { @discardableResult func updateEmail(_ newEmail: String, - withToken _: String? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { OnlineRequestProcessor.call(successHandler: onSuccess, diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 9bd3dd898..f3f4bc87e 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -67,11 +67,9 @@ struct RequestProcessor: RequestProcessorProtocol { @discardableResult func updateEmail(_ newEmail: String, - withToken token: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { chooseRequestProcessor().updateEmail(newEmail, - withToken: token, onSuccess: onSuccess, onFailure: onFailure) } diff --git a/swift-sdk/Internal/RequestProcessorProtocol.swift b/swift-sdk/Internal/RequestProcessorProtocol.swift index 31e0f6eb4..bcfa042f9 100644 --- a/swift-sdk/Internal/RequestProcessorProtocol.swift +++ b/swift-sdk/Internal/RequestProcessorProtocol.swift @@ -50,7 +50,6 @@ protocol RequestProcessorProtocol { @discardableResult func updateEmail(_ newEmail: String, - withToken _: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/OfflineRequestProcessorTests.swift index 4b0975267..01e0e0438 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/OfflineRequestProcessorTests.swift @@ -152,7 +152,6 @@ class OfflineRequestProcessorTests: XCTestCase { let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.updateEmail("new_user@example.com", - withToken: nil, onSuccess: nil, onFailure: nil) } From a67f49ef5fd99e8fb9be0f83b09dad82ed36cc4b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 21:22:20 +0530 Subject: [PATCH 45/76] Major refactoring of RequestProcessorTests. Offline processor for trackPurchase. --- swift-sdk.xcodeproj/project.pbxproj | 8 +- swift-sdk/Internal/IterableAPIInternal.swift | 2 +- .../Internal/OfflineRequestProcessor.swift | 11 +- swift-sdk/Internal/RequestProcessor.swift | 21 +- ...ests.swift => RequestProcessorTests.swift} | 214 ++++++++++++++---- 5 files changed, 200 insertions(+), 56 deletions(-) rename tests/offline-events-tests/{OfflineRequestProcessorTests.swift => RequestProcessorTests.swift} (50%) diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index dd6684fdb..740f8a8ff 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -173,7 +173,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 /* OfflineRequestProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACCF274B24F40C85004862D5 /* OfflineRequestProcessorTests.swift */; }; + 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 */; }; @@ -493,7 +493,7 @@ 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 /* OfflineRequestProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineRequestProcessorTests.swift; 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; }; @@ -1131,7 +1131,7 @@ ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */, ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, - ACCF274B24F40C85004862D5 /* OfflineRequestProcessorTests.swift */, + ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1810,7 +1810,7 @@ files = ( ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, - ACCF274C24F40C85004862D5 /* OfflineRequestProcessorTests.swift in Sources */, + ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */, ACC362C724D2C647002C67BA /* CommonMocks.swift in Sources */, ACC362C824D2C7C9002C67BA /* TestUtils.swift in Sources */, AC241C2224F5757C00F8F9CC /* Mocks.swift in Sources */, diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index f71481de5..72f096de6 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -391,8 +391,8 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { return RequestProcessor(apiKey: apiKey, authProvider: self, endPoint: config.apiEndpoint, - networkSession: networkSession, deviceMetadata: deviceMetadata, + networkSession: networkSession, notificationCenter: dependencyContainer.notificationCenter) } else { return OnlineRequestProcessor(apiKey: apiKey, diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 8e516718d..8d21444aa 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -98,7 +98,16 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackPurchaseRequest(total, + items: items, + dataFields: dataFields) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index f3f4bc87e..4bf8a1a54 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -5,14 +5,27 @@ import Foundation +protocol RequestProcessorStrategy { + var chooseOfflineProcessor: Bool { get } +} + +struct DefaultRequestProcessorStrategy: RequestProcessorStrategy { + let selectOffline: Bool + + var chooseOfflineProcessor: Bool { + selectOffline + } +} + @available(iOS 10.0, *) struct RequestProcessor: RequestProcessorProtocol { init(apiKey: String, authProvider: AuthProvider, endPoint: String, - networkSession: NetworkSessionProtocol, deviceMetadata: DeviceMetadata, - notificationCenter: NotificationCenterProtocol) { + networkSession: NetworkSessionProtocol, + notificationCenter: NotificationCenterProtocol, + strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, authProvider: authProvider, endPoint: endPoint, @@ -23,6 +36,7 @@ struct RequestProcessor: RequestProcessorProtocol { endPoint: endPoint, networkSession: networkSession, deviceMetadata: deviceMetadata) + self.strategy = strategy } @discardableResult @@ -231,10 +245,11 @@ struct RequestProcessor: RequestProcessorProtocol { onFailure: onFailure) } + private let strategy: RequestProcessorStrategy private let offlineProcessor: OfflineRequestProcessor private let onlineProcessor: OnlineRequestProcessor private func chooseRequestProcessor() -> RequestProcessorProtocol { - onlineProcessor + strategy.chooseOfflineProcessor ? offlineProcessor: onlineProcessor } } diff --git a/tests/offline-events-tests/OfflineRequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift similarity index 50% rename from tests/offline-events-tests/OfflineRequestProcessorTests.swift rename to tests/offline-events-tests/RequestProcessorTests.swift index 01e0e0438..921153c62 100644 --- a/tests/offline-events-tests/OfflineRequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import IterableSDK -class OfflineRequestProcessorTests: XCTestCase { +class RequestProcessorTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() @@ -67,7 +67,7 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.registerDeviceToken, bodyDict: bodyDict) } - + func testDisableUserforCurrentUser() throws { let hexToken = "zee-token" let bodyDict: [String: Any] = [ @@ -85,7 +85,7 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.disableDevice, bodyDict: bodyDict) } - + func testDisableUserforAllUsers() throws { let hexToken = "zee-token" let bodyDict: [String: Any] = [ @@ -102,7 +102,7 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.disableDevice, bodyDict: bodyDict) } - + func testTrackEvent() throws { let eventName = "CustomEvent1" let dataFields = ["var1": "val1", "var2": "val2"] @@ -123,7 +123,7 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.trackEvent, bodyDict: bodyDict) } - + func testUpdateUser() throws { let dataFields = ["var1": "val1", "var2": "val2"] let bodyDict: [String: Any] = [ @@ -143,7 +143,7 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.updateUser, bodyDict: bodyDict) } - + func testUpdateEmail() throws { let bodyDict: [String: Any] = [ "currentEmail": "user@example.com", @@ -160,77 +160,197 @@ class OfflineRequestProcessorTests: XCTestCase { path: Const.Path.updateEmail, bodyDict: bodyDict) } + + 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 requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackPurchase(total, + items: items, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackPurchase, + bodyDict: bodyDict) + } 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 requestProcessor = createRequestProcessor(notificationCenter: notificationCenter) + 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) } - processRequestWithSuccess(notificationCenter: notificationCenter, + let expectation1 = expectation(description: #function) + processRequestWithSuccess(request: request, + networkSession: networkSession, path: path, bodyDict: bodyDict, - request: request) - processRequestWithFailure(notificationCenter: notificationCenter, + 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, - request: request) + expectation: expectation1) + wait(for: [expectation1], timeout: 15.0) } - private func createRequestProcessor(notificationCenter: NotificationCenterProtocol) -> RequestProcessorProtocol { - OfflineRequestProcessor(apiKey: "zee-api-key", - authProvider: self, - endPoint: Endpoint.api, - deviceMetadata: deviceMetadata, - notificationCenter: notificationCenter) + 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(networkSession: networkSession, + notificationCenter: notificationCenter, + 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(networkSession: networkSession, + notificationCenter: notificationCenter, + expectation: expectation1) } - private func processRequestWithSuccess(notificationCenter: NotificationCenterProtocol, + private func createRequestProcessor(networkSession: NetworkSessionProtocol, + notificationCenter: NotificationCenterProtocol, + selectOffline: Bool) -> RequestProcessorProtocol { + RequestProcessor(apiKey: "zee-api-key", + authProvider: self, + endPoint: Endpoint.api, + deviceMetadata: deviceMetadata, + networkSession: networkSession, + notificationCenter: notificationCenter, + strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline)) + } + + private func processRequestWithSuccess(request: () -> Future, + networkSession: MockNetworkSession, path: String, bodyDict: [AnyHashable: Any], - request: () -> Future) { - let expectation1 = expectation(description: #function) + 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 - expectation1.fulfill() + expectation.fulfill() }.onError { error in XCTFail() } - - validateRequest(networkSession: MockNetworkSession(), - path: path, - bodyDict: bodyDict, - notificationCenter: notificationCenter, - expectation: expectation1) } - private func processRequestWithFailure(notificationCenter: NotificationCenterProtocol, + private func processRequestWithFailure(request: () -> Future, + networkSession: MockNetworkSession, path: String, bodyDict: [AnyHashable: Any], - request: () -> Future) { - let expectation1 = expectation(description: #function) - + 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 - expectation1.fulfill() + expectation.fulfill() } - - validateRequest(networkSession: MockNetworkSession(statusCode: 400), - path: path, - bodyDict: bodyDict, - notificationCenter: notificationCenter, - expectation: expectation1) } - private func validateRequest(networkSession: MockNetworkSession, - path: String, bodyDict: [AnyHashable : Any], - notificationCenter: NotificationCenterProtocol, - expectation: XCTestExpectation) { - networkSession.requestCallback = { request in - TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path) - XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict)) - } + private func waitForTaskRunner(networkSession: NetworkSessionProtocol, + notificationCenter: NotificationCenterProtocol, + expectation: XCTestExpectation) { let taskRunner = IterableTaskRunner(networkSession: networkSession, notificationCenter: notificationCenter, timeInterval: 0.5) @@ -250,7 +370,7 @@ class OfflineRequestProcessorTests: XCTestCase { }() } -extension OfflineRequestProcessorTests: AuthProvider { +extension RequestProcessorTests: AuthProvider { var auth: Auth { Auth(userId: nil, email: "user@example.com", authToken: nil) } From 70d81ac25769abab2a03937554b161ad2f3c1f7f Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 26 Aug 2020 22:35:16 +0530 Subject: [PATCH 46/76] Implement trackPushOpen, updateSubscriptions for OfflineRequestProcessor. --- .../Internal/OfflineRequestProcessor.swift | 27 ++++- .../RequestProcessorTests.swift | 104 ++++++++++++++---- 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 8d21444aa..96161680f 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -118,7 +118,18 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 @@ -142,7 +153,19 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { func updateSubscriptions(info: UpdateSubscriptionsInfo, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index 921153c62..ab4f71d18 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -103,27 +103,6 @@ class RequestProcessorTests: XCTestCase { bodyDict: bodyDict) } - 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 requestGenerator = { (requestProcessor: RequestProcessorProtocol) in - requestProcessor.track(event: eventName, - dataFields: dataFields, - onSuccess: nil, - onFailure: nil) - } - - try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, - path: Const.Path.trackEvent, - bodyDict: bodyDict) - } - func testUpdateUser() throws { let dataFields = ["var1": "val1", "var2": "val2"] let bodyDict: [String: Any] = [ @@ -193,6 +172,89 @@ class RequestProcessorTests: XCTestCase { bodyDict: bodyDict) } + 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 requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackPushOpen(NSNumber(value: campaignId), + templateId: NSNumber(value: templateId), + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackPushOpen, + bodyDict: bodyDict) + } + + 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 requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(event: eventName, + dataFields: dataFields, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackEvent, + bodyDict: bodyDict) + } + + 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 requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.updateSubscriptions(info: info, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.updateSubscriptions, + bodyDict: bodyDict) + } + private func processRequestWithSuccessAndFailure(requestGenerator: (RequestProcessorProtocol) -> Future, path: String, bodyDict: [AnyHashable: Any]) throws { From d14ff6fdb4a5eb2c1db9b0acbba7831620447cf9 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 27 Aug 2020 14:14:13 +0530 Subject: [PATCH 47/76] Implement all remaining OfflineRequestProcessor methods. --- .../Internal/OfflineRequestProcessor.swift | 92 +++++- tests/common/Common.swift | 7 + .../RequestProcessorTests.swift | 269 ++++++++++++++++++ 3 files changed, 359 insertions(+), 9 deletions(-) diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 96161680f..0c1270748 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -174,7 +174,16 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { inboxSessionId: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 @@ -184,7 +193,17 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { clickedUrl: String, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 @@ -195,28 +214,61 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { clickedUrl: String?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 { - fatalError() + 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 { - fatalError() + 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 { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createInAppConsumeRequest(messageId) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult @@ -225,7 +277,15 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { source: InAppDeleteSource?, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + 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 @@ -234,7 +294,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { func trackInAppOpen(_ messageId: String, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppOpenRequest(messageId) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } @discardableResult @@ -242,7 +309,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { clickedUrl: String, onSuccess: OnSuccessHandler?, onFailure: OnFailureHandler?) -> Future { - fatalError() + let requestGenerator = { (requestCreator: RequestCreator) in + requestCreator.createTrackInAppClickRequest(messageId, clickedUrl: clickedUrl) + } + + return sendIterableRequest(requestGenerator: requestGenerator, + successHandler: onSuccess, + failureHandler: onFailure, + identifier: #function) } private let apiKey: String diff --git a/tests/common/Common.swift b/tests/common/Common.swift index 41115027a..7323d5b60 100644 --- a/tests/common/Common.swift +++ b/tests/common/Common.swift @@ -45,6 +45,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/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index ab4f71d18..77e4b7d27 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -255,6 +255,272 @@ class RequestProcessorTests: XCTestCase { bodyDict: bodyDict) } + 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": deviceMetadata.asDictionary()!, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppOpen(message, + location: .inApp, + inboxSessionId: inboxSessionId, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppOpen, + bodyDict: bodyDict) + } + + 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": deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "inbox", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClick(message, + location: .inbox, + inboxSessionId: inboxSessionId, + clickedUrl: clickedUrl, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClick, + bodyDict: bodyDict) + } + + 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": deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "inbox", + "saveToInbox": false, + "silentInbox": false, + ], + "closeAction": "link", + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClose(message, location: .inbox, + inboxSessionId: inboxSessionId, + source: closeSource, + clickedUrl: clickedUrl, + onSuccess: nil, + onFailure: nil) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClose, + bodyDict: bodyDict) + } + + 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": deviceMetadata.asDictionary()!, + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(inboxSession: inboxSession, + onSuccess: nil, + onFailure: nil) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInboxSession, + bodyDict: bodyDict) + } + + 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": deviceMetadata.asDictionary()!, + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.track(inAppDelivery: message, + onSuccess: nil, + onFailure: nil) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppDelivery, + bodyDict: bodyDict) + } + + func testTrackInAppConsume() throws { + let messageId = "message_id" + + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.inAppConsume(messageId, + onSuccess: nil, + onFailure: nil) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.inAppConsume, + bodyDict: bodyDict) + } + + 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": deviceMetadata.asDictionary()!, + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.inAppConsume(message: message, + location: location, + source: source, + onSuccess: nil, + onFailure: nil) + + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.inAppConsume, + bodyDict: bodyDict) + } + + func testTrackInAppOpen2() throws { + let messageId = "message_id" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "deviceInfo": deviceMetadata.asDictionary()!, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppOpen(messageId, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppOpen, + bodyDict: bodyDict) + } + + func testTrackInAppClick2() throws { + let messageId = "message_id" + let clickedUrl = "https://somewhere.com" + let bodyDict: [String: Any] = [ + "email": "user@example.com", + "messageId": messageId, + "deviceInfo": deviceMetadata.asDictionary()!, + "clickedUrl": clickedUrl, + "messageContext": [ + "location": "in-app", + "saveToInbox": false, + "silentInbox": false, + ], + ] + + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in + requestProcessor.trackInAppClick(messageId, + clickedUrl: clickedUrl, + onSuccess: nil, + onFailure: nil) + } + + try processRequestWithSuccessAndFailure(requestGenerator: requestGenerator, + path: Const.Path.trackInAppClick, + bodyDict: bodyDict) + } + private func processRequestWithSuccessAndFailure(requestGenerator: (RequestProcessorProtocol) -> Future, path: String, bodyDict: [AnyHashable: Any]) throws { @@ -421,11 +687,14 @@ class RequestProcessorTests: XCTestCase { taskRunner.stop() } + + private 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 From 980e1b6784218128cae299ce6915307117689426 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 27 Aug 2020 21:45:15 +0530 Subject: [PATCH 48/76] Check onSuccess and onFailure for all requests. --- .../RequestProcessorTests.swift | 176 ++++++++++++++---- 1 file changed, 140 insertions(+), 36 deletions(-) diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index 77e4b7d27..d80c38a64 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -56,16 +56,20 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com" ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.register(registerTokenInfo: registerTokenInfo, notificationStateProvider: MockNotificationStateProvider(enabled: true), - onSuccess: nil, - onFailure: nil) + 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 { @@ -75,15 +79,19 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com" ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.disableDeviceForCurrentUser(hexToken: hexToken, - withOnSuccess: nil, - onFailure: nil) + 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 { @@ -92,15 +100,19 @@ class RequestProcessorTests: XCTestCase { "token": hexToken, ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.disableDeviceForAllUsers(hexToken: hexToken, - withOnSuccess: nil, - onFailure: nil) + 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 { @@ -111,16 +123,20 @@ class RequestProcessorTests: XCTestCase { "mergeNestedObjects": true ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.updateUser(dataFields, mergeNestedObjects: true, - onSuccess: nil, - onFailure: nil) + 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 { @@ -129,15 +145,19 @@ class RequestProcessorTests: XCTestCase { "newEmail": "new_user@example.com" ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.updateEmail("new_user@example.com", - onSuccess: nil, - onFailure: nil) + 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 { @@ -159,17 +179,21 @@ class RequestProcessorTests: XCTestCase { ], ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackPurchase(total, items: items, dataFields: dataFields, - onSuccess: nil, - onFailure: nil) + 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 { @@ -191,19 +215,23 @@ class RequestProcessorTests: XCTestCase { "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: nil, - onFailure: nil) + 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 { @@ -215,16 +243,20 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com" ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.track(event: eventName, dataFields: dataFields, - onSuccess: nil, - onFailure: nil) + 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 { @@ -244,15 +276,19 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com" ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.updateSubscriptions(info: info, - onSuccess: nil, - onFailure: nil) + 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 { @@ -271,17 +307,21 @@ class RequestProcessorTests: XCTestCase { ], ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackInAppOpen(message, location: .inApp, inboxSessionId: inboxSessionId, - onSuccess: nil, - onFailure: nil) + 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 { @@ -302,18 +342,22 @@ class RequestProcessorTests: XCTestCase { ], ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackInAppClick(message, location: .inbox, inboxSessionId: inboxSessionId, clickedUrl: clickedUrl, - onSuccess: nil, - onFailure: nil) + 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 { @@ -336,19 +380,23 @@ class RequestProcessorTests: XCTestCase { "closeAction": "link", ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackInAppClose(message, location: .inbox, inboxSessionId: inboxSessionId, source: closeSource, clickedUrl: clickedUrl, - onSuccess: nil, - onFailure: nil) + 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 { @@ -381,16 +429,20 @@ class RequestProcessorTests: XCTestCase { "deviceInfo": deviceMetadata.asDictionary()!, ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.track(inboxSession: inboxSession, - onSuccess: nil, - onFailure: nil) + 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 { @@ -407,16 +459,20 @@ class RequestProcessorTests: XCTestCase { "deviceInfo": deviceMetadata.asDictionary()!, ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.track(inAppDelivery: message, - onSuccess: nil, - onFailure: nil) + 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 { @@ -427,16 +483,20 @@ class RequestProcessorTests: XCTestCase { "messageId": messageId, ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.inAppConsume(messageId, - onSuccess: nil, - onFailure: nil) + 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 { @@ -456,18 +516,22 @@ class RequestProcessorTests: XCTestCase { "deviceInfo": deviceMetadata.asDictionary()!, ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.inAppConsume(message: message, location: location, source: source, - onSuccess: nil, - onFailure: nil) + 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 { @@ -483,15 +547,19 @@ class RequestProcessorTests: XCTestCase { ], ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackInAppOpen(messageId, - onSuccess: nil, - onFailure: nil) + 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 { @@ -509,16 +577,20 @@ class RequestProcessorTests: XCTestCase { ], ] + let expectations = createExpectations(description: #function) + let requestGenerator = { (requestProcessor: RequestProcessorProtocol) in requestProcessor.trackInAppClick(messageId, clickedUrl: clickedUrl, - onSuccess: nil, - onFailure: nil) + 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, @@ -687,8 +759,40 @@ class RequestProcessorTests: XCTestCase { taskRunner.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 let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), platform: JsonValue.iOS.jsonStringValue, appPackageName: Bundle.main.appPackageName ?? "") From 0e8d2e3fdb8fe10d67a464af7fdc3da21dd4c05c Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 2 Sep 2020 14:09:01 +0530 Subject: [PATCH 49/76] Merge auth related fix to offline/online request processors. --- swift-sdk/Internal/IterableAPIInternal.swift | 12 +- .../Internal/OfflineRequestProcessor.swift | 4 + .../Internal/OnlineRequestProcessor.swift | 211 ++++++++---------- swift-sdk/Internal/RequestProcessor.swift | 3 + swift-sdk/Internal/RequestProcessorUtil.swift | 13 +- .../RequestProcessorTests.swift | 1 + 6 files changed, 116 insertions(+), 128 deletions(-) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 72f096de6..01818de45 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -389,14 +389,16 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { lazy var requestProcessor: RequestProcessorProtocol = { if #available(iOS 10.0, *) { return RequestProcessor(apiKey: apiKey, - authProvider: self, - endPoint: config.apiEndpoint, - deviceMetadata: deviceMetadata, - networkSession: networkSession, - notificationCenter: dependencyContainer.notificationCenter) + authProvider: self, + authFailureDelegate: config.authFailureDelegate, + endPoint: config.apiEndpoint, + deviceMetadata: deviceMetadata, + networkSession: networkSession, + notificationCenter: dependencyContainer.notificationCenter) } else { return OnlineRequestProcessor(apiKey: apiKey, authProvider: self, + authFailureDelegate: config.authFailureDelegate, endPoint: config.apiEndpoint, networkSession: networkSession, deviceMetadata: deviceMetadata) diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 0c1270748..58c1ec71f 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -9,11 +9,13 @@ import Foundation struct OfflineRequestProcessor: RequestProcessorProtocol { init(apiKey: String, authProvider: AuthProvider, + authFailureDelegate: IterableAuthFailureDelegate?, endPoint: String, deviceMetadata: DeviceMetadata, notificationCenter: NotificationCenterProtocol) { self.apiKey = apiKey self.authProvider = authProvider + self.authFailureDelegate = authFailureDelegate self.endPoint = endPoint self.deviceMetadata = deviceMetadata notificationListener = NotificationListener(notificationCenter: notificationCenter) @@ -321,6 +323,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { private let apiKey: String private weak var authProvider: AuthProvider? + private weak var authFailureDelegate: IterableAuthFailureDelegate? private let endPoint: String private let deviceMetadata: DeviceMetadata private let notificationListener: NotificationListener @@ -354,6 +357,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { let result = notificationListener.futureFromTask(withTaskId: taskId) return RequestProcessorUtil.apply(successHandler: onSuccess, andFailureHandler: onFailure, + andAuthFailureHandler: authFailureDelegate, toResult: result, withIdentifier: identifier) } catch let error { diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index 0dc65b8b6..5f59b228c 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -9,9 +9,11 @@ import Foundation struct OnlineRequestProcessor: RequestProcessorProtocol { init(apiKey: String, authProvider: AuthProvider, + authFailureDelegate: IterableAuthFailureDelegate?, endPoint: String, networkSession: NetworkSessionProtocol, deviceMetadata: DeviceMetadata) { + self.authFailureDelegate = authFailureDelegate apiClient = ApiClient(apiKey: apiKey, authProvider: authProvider, endPoint: endPoint, @@ -49,20 +51,20 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { mergeNestedObjects: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateUser", - forResult: apiClient.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects)) + 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 { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "updateEmail", - forResult: apiClient.updateEmail(newEmail: newEmail)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "updateEmail", + forResult: apiClient.updateEmail(newEmail: newEmail)) } @discardableResult @@ -71,10 +73,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPurchase", - forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackPurchase", + forResult: apiClient.track(purchase: total, items: items, dataFields: dataFields)) } @discardableResult @@ -85,14 +87,14 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackPushOpen", - forResult: apiClient.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackPushOpen", + forResult: apiClient.track(pushOpen: campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: dataFields)) } @discardableResult @@ -100,25 +102,25 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackEvent", - forResult: apiClient.track(event: event, dataFields: dataFields)) + 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 { - OnlineRequestProcessor.call(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)) + 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 @@ -128,10 +130,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId)) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) } @discardableResult @@ -143,10 +145,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppClick: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), clickedUrl: clickedUrl) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: result) } @discardableResult @@ -160,10 +162,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { let result = apiClient.track(inAppClose: InAppMessageContext.from(message: message, location: location, inboxSessionId: inboxSessionId), source: source, clickedUrl: clickedUrl) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClose", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClose", + forResult: result) } @discardableResult @@ -172,30 +174,30 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inboxSession: inboxSession) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInboxSession", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInboxSession", + forResult: result) } @discardableResult func track(inAppDelivery message: IterableInAppMessage, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppDelivery", - forResult: apiClient.track(inAppDelivery: InAppMessageContext.from(message: message, location: nil))) + 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 { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsume", - forResult: apiClient.inAppConsume(messageId: messageId)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "inAppConsume", + forResult: apiClient.inAppConsume(messageId: messageId)) } @discardableResult @@ -206,10 +208,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.inAppConsume(inAppMessageContext: InAppMessageContext.from(message: message, location: location), source: source) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "inAppConsumeWithSource", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "inAppConsumeWithSource", + forResult: result) } // MARK: DEPRECATED @@ -219,10 +221,10 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { let result = apiClient.track(inAppOpen: messageId) - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppOpen", - forResult: result) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppOpen", + forResult: result) } @discardableResult @@ -230,24 +232,25 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { clickedUrl: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "trackInAppClick", - forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "trackInAppClick", + forResult: apiClient.track(inAppClick: messageId, clickedUrl: clickedUrl)) } private let apiClient: ApiClientProtocol + private weak var authFailureDelegate: IterableAuthFailureDelegate? @discardableResult private func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - return OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "registerToken", - forResult: apiClient.register(registerTokenInfo: registerTokenInfo, - notificationsEnabled: notificationsEnabled)) + return applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "registerToken", + forResult: apiClient.register(registerTokenInfo: registerTokenInfo, + notificationsEnabled: notificationsEnabled)) } @discardableResult @@ -255,54 +258,20 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { hexToken: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Future { - OnlineRequestProcessor.call(successHandler: onSuccess, - andFailureHandler: onFailure, - withIdentifier: "disableDevice", - forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) + applyCallbacks(successHandler: onSuccess, + andFailureHandler: onFailure, + withIdentifier: "disableDevice", + forResult: apiClient.disableDevice(forAllUsers: allUsers, hexToken: hexToken)) } - // TODO: @tqm, replace with RequestProcessorUtil.apply(...) - @discardableResult - private static func call(successHandler onSuccess: OnSuccessHandler? = nil, - andFailureHandler onFailure: OnFailureHandler? = nil, - withIdentifier identifier: String, - forResult result: Future) -> Future { - result.onSuccess { json in - if let onSuccess = onSuccess { - onSuccess(json) - } else { - defaultOnSuccess(identifier)(json) - } - }.onError { error in - if let onFailure = onFailure { - onFailure(error.reason, error.data) - } else { - defaultOnFailure(identifier)(error.reason, error.data) - } - } - return result - } - - 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) - } + private func applyCallbacks(successHandler onSuccess: OnSuccessHandler? = nil, + andFailureHandler onFailure: OnFailureHandler? = nil, + withIdentifier identifier: String, + forResult result: Future) -> Future { + RequestProcessorUtil.apply(successHandler: onSuccess, + andFailureHandler: onFailure, + andAuthFailureHandler: authFailureDelegate, + toResult: result, + withIdentifier: identifier) } } diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 4bf8a1a54..8845c47b0 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -21,6 +21,7 @@ struct DefaultRequestProcessorStrategy: RequestProcessorStrategy { struct RequestProcessor: RequestProcessorProtocol { init(apiKey: String, authProvider: AuthProvider, + authFailureDelegate: IterableAuthFailureDelegate?, endPoint: String, deviceMetadata: DeviceMetadata, networkSession: NetworkSessionProtocol, @@ -28,11 +29,13 @@ struct RequestProcessor: RequestProcessorProtocol { strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, authProvider: authProvider, + authFailureDelegate: authFailureDelegate, endPoint: endPoint, deviceMetadata: deviceMetadata, notificationCenter: notificationCenter) onlineProcessor = OnlineRequestProcessor(apiKey: apiKey, authProvider: authProvider, + authFailureDelegate: authFailureDelegate, endPoint: endPoint, networkSession: networkSession, deviceMetadata: deviceMetadata) diff --git a/swift-sdk/Internal/RequestProcessorUtil.swift b/swift-sdk/Internal/RequestProcessorUtil.swift index fbe30e524..76f3654c4 100644 --- a/swift-sdk/Internal/RequestProcessorUtil.swift +++ b/swift-sdk/Internal/RequestProcessorUtil.swift @@ -9,6 +9,7 @@ struct RequestProcessorUtil { @discardableResult static func apply(successHandler onSuccess: OnSuccessHandler? = nil, andFailureHandler onFailure: OnFailureHandler? = nil, + andAuthFailureHandler onAuthFailure: IterableAuthFailureDelegate? = nil, toResult result: Future, withIdentifier identifier: String) -> Future { result.onSuccess { json in @@ -18,6 +19,14 @@ struct RequestProcessorUtil { defaultOnSuccess(identifier)(json) } }.onError { error in + if let onAuthFailure = onAuthFailure, + error.httpStatusCode == 401, + error.iterableCode == JsonValue.Code.invalidJwtPayload { + onAuthFailure.authTokenFailed() + } else { + ITBError("\(identifier) failed authorization.") + } + if let onFailure = onFailure { onFailure(error.reason, error.data) } else { @@ -27,7 +36,7 @@ struct RequestProcessorUtil { return result } - static func defaultOnSuccess(_ identifier: String) -> OnSuccessHandler { + private static func defaultOnSuccess(_ identifier: String) -> OnSuccessHandler { { data in if let data = data { ITBInfo("\(identifier) succeeded, got response: \(data)") @@ -37,7 +46,7 @@ struct RequestProcessorUtil { } } - static func defaultOnFailure(_ identifier: String) -> OnFailureHandler { + private static func defaultOnFailure(_ identifier: String) -> OnFailureHandler { { reason, data in var toLog = "\(identifier) failed:" if let reason = reason { diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index d80c38a64..09c4b50b8 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -708,6 +708,7 @@ class RequestProcessorTests: XCTestCase { selectOffline: Bool) -> RequestProcessorProtocol { RequestProcessor(apiKey: "zee-api-key", authProvider: self, + authFailureDelegate: nil, endPoint: Endpoint.api, deviceMetadata: deviceMetadata, networkSession: networkSession, From 0357068f64e827571f10f04f6d69e8b5a7883451 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 2 Sep 2020 21:14:16 +0530 Subject: [PATCH 50/76] NetworkHelper rewrite using generic object type. --- swift-sdk/Internal/NetworkHelper.swift | 173 ++++++++++++------ .../IterableAPIResponseTests.swift | 2 +- 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 2751531a0..97f8bc801 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -33,11 +33,44 @@ 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 } } @@ -87,8 +120,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 +143,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 +168,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/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) From 51471426a542166be48530d2a661a84fcc81a3be Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 8 Sep 2020 14:37:49 +0530 Subject: [PATCH 51/76] NetworkConnectivityChecker struct to check if network is available. --- swift-sdk.xcodeproj/project.pbxproj | 8 ++ swift-sdk/Internal/DataFieldsHelper.swift | 2 +- swift-sdk/Internal/IterablePersistence.swift | 2 +- swift-sdk/Internal/IterableTaskError.swift | 2 +- .../Internal/NetworkConnectivityChecker.swift | 113 ++++++++++++++++++ swift-sdk/Internal/NetworkHelper.swift | 13 ++ tests/common/CommonMocks.swift | 34 ++++++ .../NetworkConnectivityCheckerTests.swift | 58 +++++++++ 8 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 swift-sdk/Internal/NetworkConnectivityChecker.swift create mode 100644 tests/offline-events-tests/NetworkConnectivityCheckerTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 740f8a8ff..7db3169a8 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 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 */; }; @@ -133,6 +134,7 @@ 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 */; }; @@ -417,6 +419,7 @@ 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 = ""; }; @@ -466,6 +469,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 = ""; }; @@ -855,6 +859,7 @@ AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */, AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */, + AC978D3D24FF953C00372B8C /* NetworkConnectivityChecker.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -1132,6 +1137,7 @@ ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */, AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, + AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1654,6 +1660,7 @@ 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 */, @@ -1808,6 +1815,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AC67AF982507481200C1E974 /* NetworkConnectivityCheckerTests.swift in Sources */, ACFD5AC624C8216A008E497A /* TasksCRUDTests.swift in Sources */, ACC362C624D2C334002C67BA /* CommonExtensions.swift in Sources */, ACCF274C24F40C85004862D5 /* RequestProcessorTests.swift in Sources */, 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/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index 77daa7fd6..47b523ac0 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -12,7 +12,7 @@ enum IterableDBError: Error { } extension IterableDBError: LocalizedError { - var localizedDescription: String { + var errorDescription: String? { switch self { case let .general(description): return description diff --git a/swift-sdk/Internal/IterableTaskError.swift b/swift-sdk/Internal/IterableTaskError.swift index e406637d5..9c73bf2fc 100644 --- a/swift-sdk/Internal/IterableTaskError.swift +++ b/swift-sdk/Internal/IterableTaskError.swift @@ -14,7 +14,7 @@ enum IterableTaskError: Error { } extension IterableTaskError: LocalizedError { - var localizedDescription: String { + var errorDescription: String? { switch self { case let .general(description): return description ?? "general error" 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/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 97f8bc801..c69bacaee 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -74,10 +74,19 @@ extension NetworkError: LocalizedError { } } +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 { @@ -96,6 +105,10 @@ extension URLSession: NetworkSessionProtocol { task.resume() } + + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { + dataTask(with: url, completionHandler: completionHandler) + } } struct NetworkHelper { diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index 99dda22a0..ae06dd4c0 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -152,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? @@ -206,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!) } @@ -248,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 { diff --git a/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift new file mode 100644 index 000000000..e2c7ec4e9 --- /dev/null +++ b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift @@ -0,0 +1,58 @@ +// +// Created by Tapash Majumder on 9/8/20. +// Copyright © 2020 Iterable. All rights reserved. +// + +import XCTest + +@testable import IterableSDK + +class NetworkConnectivityCheckerTests: XCTestCase { + 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) + } +} From 9f10d10f8031812e32eab4ca59f571e58edc2f68 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 9 Sep 2020 14:40:38 +0530 Subject: [PATCH 52/76] Add NetworkConnectivityManager and tests. --- swift-sdk.xcodeproj/project.pbxproj | 24 +++- .../Internal/NetworkConnectivityManager.swift | 86 ++++++++++++ swift-sdk/Internal/NetworkMonitor.swift | 88 ++++++++++++ .../NetworkConnectivityCheckerTests.swift | 6 + .../NetworkConnectivityManagerTests.swift | 128 ++++++++++++++++++ 5 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 swift-sdk/Internal/NetworkConnectivityManager.swift create mode 100644 swift-sdk/Internal/NetworkMonitor.swift create mode 100644 tests/offline-events-tests/NetworkConnectivityManagerTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 7db3169a8..80992edd8 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -194,6 +194,9 @@ 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 */; }; @@ -518,6 +521,9 @@ 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 = ""; }; @@ -859,7 +865,6 @@ AC5812F524F3A90F007E6D36 /* OfflineRequestProcessor.swift */, AC5812F724F3AE8D007E6D36 /* RequestProcessor.swift */, AC3A336C24F65579008225BA /* RequestProcessorUtil.swift */, - AC978D3D24FF953C00372B8C /* NetworkConnectivityChecker.swift */, ); name = "Request Processing"; sourceTree = ""; @@ -879,6 +884,7 @@ AC72A0BB20CF4C8C004D7997 /* Internal */ = { isa = PBXGroup; children = ( + ACF4061F25078186005FD775 /* Network */, AC1AA1C724EBB39500F29C6B /* Notification Center */, AC5E888724E1B7AD00752321 /* Request Processing */, ACC362BB24D21153002C67BA /* Task Processing */, @@ -898,7 +904,6 @@ AC72A0C520CF4CB9004D7997 /* IterableAPIInternal.swift */, AC2C668120D32F2800D46CC9 /* IterableAppIntegrationInternal.swift */, AC72A0BD20CF4C98004D7997 /* IterableDeepLinkManager.swift */, - ACD6116B2107D004003E7F6B /* NetworkHelper.swift */, AC2B79F621E6A38900A59080 /* NotificationHelper.swift */, ACEDF41C2183C2EC000B9BFE /* Promise.swift */, ); @@ -1102,6 +1107,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 = ( @@ -1138,6 +1154,7 @@ AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */, ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */, + ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1605,6 +1622,7 @@ 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 */, @@ -1622,6 +1640,7 @@ 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 */, @@ -1818,6 +1837,7 @@ 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 */, diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift new file mode 100644 index 000000000..ea782566c --- /dev/null +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -0,0 +1,86 @@ +// +// 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) { + ITBInfo() + self.networkMonitor = networkMonitor ?? Self.createNetworkMonitor() + self.connectivityChecker = connectivityChecker + self.notificationCenter = notificationCenter + super.init() + notificationCenter.addObserver(self, + selector: #selector(onAppWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(onAppDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + } + + deinit { + ITBInfo() + notificationCenter.removeObserver(self) + } + + var isOnline: Bool { + online + } + + var connectivityChangedCallback: ((Bool) -> Void)? + + func start() { + ITBInfo() + networkMonitor.statusUpdatedCallback = updateStatus + networkMonitor.start() + } + + func stop() { + networkMonitor.stop() + } + + private func updateStatus() { + ITBInfo() + connectivityChecker.checkConnectivity().onSuccess { connected in + self.online = connected + } + } + + @objc + private func onAppWillEnterForeground(notification _: Notification) { + ITBInfo() + start() + } + + @objc + private func onAppDidEnterBackground(notification _: Notification) { + ITBInfo() + stop() + } + + 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 online = true { + didSet { + if online != oldValue { + connectivityChangedCallback?(online) + } + } + } +} diff --git a/swift-sdk/Internal/NetworkMonitor.swift b/swift-sdk/Internal/NetworkMonitor.swift new file mode 100644 index 000000000..38f3a9c7a --- /dev/null +++ b/swift-sdk/Internal/NetworkMonitor.swift @@ -0,0 +1,88 @@ +// +// 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() + } + + 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() + } + + private 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/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift index e2c7ec4e9..2fe4b453d 100644 --- a/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift +++ b/tests/offline-events-tests/NetworkConnectivityCheckerTests.swift @@ -8,6 +8,12 @@ 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() diff --git a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift new file mode 100644 index 000000000..efda7b1cc --- /dev/null +++ b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift @@ -0,0 +1,128 @@ +// +// 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) + } +} From d70d6a9f21f6f633694ffabc0ab1ec6b26ebcbcb Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 9 Sep 2020 15:16:14 +0530 Subject: [PATCH 53/76] Unit tests for foreground/background change. --- .../NetworkConnectivityManagerTests.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift index efda7b1cc..48c6e2891 100644 --- a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift +++ b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift @@ -125,4 +125,47 @@ class NetworkConnectivityManagerTests: XCTestCase { } wait(for: [expectation3], timeout: 1.0) } + + 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) + + // 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 still offline when app is in background, even though network is normal. + notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil, userInfo: nil) + let expectation2 = expectation(description: "ConnectivityManager: check no status change on network back to normal") + expectation2.isInverted = true + manager.connectivityChangedCallback = { connected in + XCTAssertTrue(connected) + expectation2.fulfill() + } + networkSession.error = nil + wait(for: [expectation2], timeout: 1.0) + + // check that status changes when we go online + let expectation3 = expectation(description: "ConnectivityManager: status change when app goes to foreground") + manager.connectivityChangedCallback = { connected in + XCTAssertTrue(connected) + expectation3.fulfill() + } + notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil, userInfo: nil) + wait(for: [expectation3], timeout: 10.0) + } } From 3fa089c9f167026122f0a99b98a4facdf18f81dd Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 9 Sep 2020 19:56:21 +0530 Subject: [PATCH 54/76] More tests and events for online/offline mode. --- .../Internal/IterableNotifications.swift | 2 + .../Internal/NetworkConnectivityManager.swift | 59 +++++++++++++++- .../NetworkConnectivityManagerTests.swift | 70 +++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Internal/IterableNotifications.swift b/swift-sdk/Internal/IterableNotifications.swift index 902cafa23..e3a9b3744 100644 --- a/swift-sdk/Internal/IterableNotifications.swift +++ b/swift-sdk/Internal/IterableNotifications.swift @@ -18,6 +18,8 @@ extension Notification.Name { 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 { diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index ea782566c..208aa3b20 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -8,11 +8,15 @@ import Foundation class NetworkConnectivityManager: NSObject { init(networkMonitor: NetworkMonitorProtocol? = nil, connectivityChecker: NetworkConnectivityChecker = NetworkConnectivityChecker(), - notificationCenter: NotificationCenterProtocol = NotificationCenter.default) { + 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(onAppWillEnterForeground(notification:)), @@ -22,11 +26,20 @@ class NetworkConnectivityManager: NSObject { selector: #selector(onAppDidEnterBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil) + 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) + stopTimer() } var isOnline: Bool { @@ -39,12 +52,38 @@ class NetworkConnectivityManager: NSObject { ITBInfo() networkMonitor.statusUpdatedCallback = 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 @@ -64,6 +103,18 @@ class NetworkConnectivityManager: NSObject { stop() } + @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() @@ -75,11 +126,17 @@ class NetworkConnectivityManager: NSObject { 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 { if online != oldValue { connectivityChangedCallback?(online) + resetTimer() } } } diff --git a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift index 48c6e2891..63711ce11 100644 --- a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift +++ b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift @@ -168,4 +168,74 @@ class NetworkConnectivityManagerTests: XCTestCase { notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil, userInfo: nil) wait(for: [expectation3], timeout: 10.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) + } } From 749c627b5ddb7b3b2b7f07851d27f0feb220ee14 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Sat, 12 Sep 2020 15:54:39 +0530 Subject: [PATCH 55/76] Add ConnectivityManager to TaskRunner --- swift-sdk/Internal/IterableTaskRunner.swift | 72 +++++++++++---- .../Internal/NetworkConnectivityManager.swift | 2 + tests/common/CommonMocks.swift | 4 + .../TaskRunnerTests.swift | 88 ++++++++++++++++++- 4 files changed, 148 insertions(+), 18 deletions(-) diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 0ffe19921..2dc158a45 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -11,47 +11,81 @@ class IterableTaskRunner: NSObject { init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), notificationCenter: NotificationCenterProtocol = NotificationCenter.default, - timeInterval: TimeInterval = 1.0 * 60) { + 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.connectivityManager.connectivityChangedCallback = onConnectivityChanged(connected:) } func start() { ITBInfo() paused = false run() + connectivityManager.start() } func stop() { ITBInfo() paused = true timer?.invalidate() + connectivityManager.stop() } @objc private func onTaskScheduled(notification: Notification) { ITBInfo() if !running && !paused { - timer?.invalidate() - run() + runNow() + } + } + + private func runNow() { + timer?.invalidate() + 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") + ITBInfo("Done processing tasks") self.running = false self.scheduleNext() } @@ -60,16 +94,19 @@ class IterableTaskRunner: NSObject { private func scheduleNext() { ITBInfo() - if !self.paused { - DispatchQueue.global().async { - ITBInfo("scheduling timer") - let timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: false) { _ in - self.run() - } - self.timer = timer - RunLoop.current.add(timer, forMode: .default) - RunLoop.current.run() + guard !paused else { + ITBInfo("Paused") + return + } + + DispatchQueue.global().async { + ITBInfo("Scheduling timer") + let timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: false) { _ in + self.run() } + self.timer = timer + RunLoop.current.add(timer, forMode: .default) + RunLoop.current.run() } } @@ -78,8 +115,10 @@ class IterableTaskRunner: NSObject { ITBInfo() running = true + /// This is a recursive function. + /// Check whether we were stopped in the middle of running tasks guard !paused else { - ITBInfo("paused") + ITBInfo("Tasks paused before finishing processTasks()") return Promise(value: ()) } @@ -101,7 +140,7 @@ class IterableTaskRunner: NSObject { @discardableResult private func execute(task: IterableTask) -> Future { - ITBInfo("executing taskId: \(task.id)") + ITBInfo("Executing taskId: \(task.id)") guard task.processing == false else { return Promise(value: .processing) } @@ -184,6 +223,7 @@ class IterableTaskRunner: NSObject { private let persistenceContextProvider: IterablePersistenceContextProvider private let notificationCenter: NotificationCenterProtocol private let timeInterval: TimeInterval + private let connectivityManager: NetworkConnectivityManager private var timer: Timer? private var running = false diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index 208aa3b20..db5a24fa2 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -134,7 +134,9 @@ class NetworkConnectivityManager: NSObject { private var online = true { didSet { + ITBInfo("online: \(online)") if online != oldValue { + ITBInfo() connectivityChangedCallback?(online) resetTimer() } diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index ae06dd4c0..7ff6c6114 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -417,6 +417,10 @@ class MockNotificationCenter: NotificationCenterProtocol { addObserver(callbackClass, selector: #selector(callbackClass.onNotification(notification:)), name: notification, object: self) } + func clearCallbacks() { + observers.removeAll() + } + private class Observer: NSObject { let observer: NSObject let notificationName: Notification.Name diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index c244c7886..190922a7d 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -114,7 +114,65 @@ class TaskRunnerTests: XCTestCase { 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, + 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, + 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() + } + private func scheduleSampleTask(notificationCenter: NotificationCenterProtocol) throws -> String { let apiKey = "zee-api-key" let eventName = "CustomEvent1" @@ -135,7 +193,33 @@ class TaskRunnerTests: XCTestCase { notificationCenter: notificationCenter, dateProvider: dateProvider).schedule(apiCallRequest: apiCallRequest) } - + + private func verifyNoTaskIsExecuted(_ notificationCenter: MockNotificationCenter, forInterval interval: TimeInterval) { + let expectation1 = expectation(description: "Wait for task complete notification.") + expectation1.isInverted = true + + notificationCenter.clearCallbacks() + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithRetry) { _ in + XCTFail() + } + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithNoRetry) { _ in + XCTFail() + } + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + XCTFail() + } + wait(for: [expectation1], timeout: interval) + } + + private func verifyTaskIsExecuted(_ notificationCenter: MockNotificationCenter, withinInterval interval: TimeInterval) { + let expectation1 = expectation(description: "Wait for task complete notification.") + notificationCenter.clearCallbacks() + notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + expectation1.fulfill() + } + wait(for: [expectation1], timeout: interval) + } + private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), platform: JsonValue.iOS.jsonStringValue, appPackageName: Bundle.main.appPackageName ?? "") From 7421348611b0052ddb4142df936567b2446ef081 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Sat, 12 Sep 2020 21:10:10 +0530 Subject: [PATCH 56/76] Move moving to foreground, background processing to TaskRunner from ConnectivityManager. --- swift-sdk/Internal/IterableTaskRunner.swift | 20 ++++++++ .../Internal/NetworkConnectivityManager.swift | 20 -------- tests/common/CommonMocks.swift | 31 +++++++++---- .../NetworkConnectivityManagerTests.swift | 43 ----------------- .../TaskRunnerTests.swift | 46 ++++++++++++++++--- 5 files changed, 83 insertions(+), 77 deletions(-) diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 2dc158a45..e7b736416 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -26,6 +26,14 @@ class IterableTaskRunner: NSObject { 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 = onConnectivityChanged(connected:) } @@ -51,6 +59,18 @@ class IterableTaskRunner: NSObject { } } + @objc + private func onAppWillEnterForeground(notification _: Notification) { + ITBInfo() + start() + } + + @objc + private func onAppDidEnterBackground(notification _: Notification) { + ITBInfo() + stop() + } + private func runNow() { timer?.invalidate() run() diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index db5a24fa2..8ea2bd9f4 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -18,14 +18,6 @@ class NetworkConnectivityManager: NSObject { self.offlineModePollingInterval = offlineModePollingInterval ?? Self.defaultOfflineModePollingInterval self.onlineModePollingInterval = onlineModePollingInterval ?? Self.defaultOnlineModePollingInterval super.init() - notificationCenter.addObserver(self, - selector: #selector(onAppWillEnterForeground(notification:)), - name: UIApplication.willEnterForegroundNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(onAppDidEnterBackground(notification:)), - name: UIApplication.didEnterBackgroundNotification, - object: nil) notificationCenter.addObserver(self, selector: #selector(onNetworkOnline(notification:)), name: .iterableNetworkOnline, @@ -91,18 +83,6 @@ class NetworkConnectivityManager: NSObject { } } - @objc - private func onAppWillEnterForeground(notification _: Notification) { - ITBInfo() - start() - } - - @objc - private func onAppDidEnterBackground(notification _: Notification) { - ITBInfo() - stop() - } - @objc private func onNetworkOnline(notification _: Notification) { ITBInfo() diff --git a/tests/common/CommonMocks.swift b/tests/common/CommonMocks.swift index 7ff6c6114..c0f0e9f01 100644 --- a/tests/common/CommonMocks.swift +++ b/tests/common/CommonMocks.swift @@ -389,7 +389,9 @@ 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) {} @@ -401,9 +403,11 @@ class MockNotificationCenter: NotificationCenterProtocol { } } - func addCallback(forNotification notification: Notification.Name, callback: @escaping (Notification) -> Void) { + @discardableResult + func addCallback(forNotification notification: Notification.Name, callback: @escaping (Notification) -> Void) -> String { class CallbackClass: NSObject { let callback: (Notification) -> Void + init(callback: @escaping (Notification) -> Void) { self.callback = callback } @@ -412,21 +416,32 @@ class MockNotificationCenter: NotificationCenterProtocol { 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 clearCallbacks() { - observers.removeAll() + + 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/offline-events-tests/NetworkConnectivityManagerTests.swift b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift index 63711ce11..cd36689d6 100644 --- a/tests/offline-events-tests/NetworkConnectivityManagerTests.swift +++ b/tests/offline-events-tests/NetworkConnectivityManagerTests.swift @@ -126,49 +126,6 @@ class NetworkConnectivityManagerTests: XCTestCase { wait(for: [expectation3], timeout: 1.0) } - 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) - - // 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 still offline when app is in background, even though network is normal. - notificationCenter.post(name: UIApplication.didEnterBackgroundNotification, object: nil, userInfo: nil) - let expectation2 = expectation(description: "ConnectivityManager: check no status change on network back to normal") - expectation2.isInverted = true - manager.connectivityChangedCallback = { connected in - XCTAssertTrue(connected) - expectation2.fulfill() - } - networkSession.error = nil - wait(for: [expectation2], timeout: 1.0) - - // check that status changes when we go online - let expectation3 = expectation(description: "ConnectivityManager: status change when app goes to foreground") - manager.connectivityChangedCallback = { connected in - XCTAssertTrue(connected) - expectation3.fulfill() - } - notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil, userInfo: nil) - wait(for: [expectation3], timeout: 10.0) - } - func testOnlinePollingInterval() throws { // Network status will never be updated class NoUpdateNetworkMonitor: NetworkMonitorProtocol { diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index 190922a7d..040fb3086 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -172,6 +172,40 @@ class TaskRunnerTests: XCTestCase { 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, + 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" @@ -198,26 +232,26 @@ class TaskRunnerTests: XCTestCase { let expectation1 = expectation(description: "Wait for task complete notification.") expectation1.isInverted = true - notificationCenter.clearCallbacks() - notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithRetry) { _ in + let id1 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithRetry) { _ in XCTFail() } - notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithNoRetry) { _ in + let id2 = notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithNoRetry) { _ in XCTFail() } - notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + 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.") - notificationCenter.clearCallbacks() - notificationCenter.addCallback(forNotification: .iterableTaskFinishedWithSuccess) { _ in + 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(), From 8d6ff496c7a27e831d56c66c67ed4d1d90fddd3e Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 14 Sep 2020 11:18:13 +0530 Subject: [PATCH 57/76] 1. No need to reset timer. 2. Add import UIKit. --- swift-sdk/Internal/IterableTaskRunner.swift | 2 ++ swift-sdk/Internal/NetworkConnectivityManager.swift | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index e7b736416..ded52ebc3 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -4,6 +4,8 @@ // import Foundation +import UIKit + @available(iOS 10.0, *) class IterableTaskRunner: NSObject { diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index 8ea2bd9f4..784b1b1a1 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -116,10 +116,10 @@ class NetworkConnectivityManager: NSObject { didSet { ITBInfo("online: \(online)") if online != oldValue { - ITBInfo() + ITBInfo("connectivity changed") connectivityChangedCallback?(online) - resetTimer() } } } } + From 21d71c9f15fd46a36c53b3698b2b631486a0b599 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 15 Sep 2020 01:30:47 +0530 Subject: [PATCH 58/76] Need to reset timer when online offline mode changes. --- swift-sdk/Internal/NetworkConnectivityManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index 784b1b1a1..424cf0de6 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -118,6 +118,7 @@ class NetworkConnectivityManager: NSObject { if online != oldValue { ITBInfo("connectivity changed") connectivityChangedCallback?(online) + resetTimer() } } } From 7c1b70fdb8dda35350bb9f1c5416a8fee9fac95f Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 15 Sep 2020 13:48:55 +0530 Subject: [PATCH 59/76] Add name field to IterableTask --- swift-sdk/Internal/IterableTask.swift | 4 ++++ swift-sdk/Internal/IterableTaskManagedObject.swift | 1 + 2 files changed, 5 insertions(+) diff --git a/swift-sdk/Internal/IterableTask.swift b/swift-sdk/Internal/IterableTask.swift index b3072c75d..1c15cd192 100644 --- a/swift-sdk/Internal/IterableTask.swift +++ b/swift-sdk/Internal/IterableTask.swift @@ -9,6 +9,7 @@ struct IterableTask { static let currentVersion = 1 let id: String + let name: String? let version: Int let createdAt: Date? let modifiedAt: Date? @@ -24,6 +25,7 @@ struct IterableTask { let taskFailureData: Data? init(id: String, + name: String? = nil, version: Int = IterableTask.currentVersion, createdAt: Date? = nil, modifiedAt: Date? = nil, @@ -38,6 +40,7 @@ struct IterableTask { requestedAt: Date, taskFailureData: Data? = nil) { self.id = id + self.name = name self.version = version self.createdAt = createdAt self.modifiedAt = modifiedAt @@ -61,6 +64,7 @@ struct IterableTask { failed: Bool? = nil, taskFailureData: Data? = nil) -> IterableTask { IterableTask(id: id, + name: name, version: version, createdAt: createdAt, modifiedAt: modifiedAt, diff --git a/swift-sdk/Internal/IterableTaskManagedObject.swift b/swift-sdk/Internal/IterableTaskManagedObject.swift index 132d9817c..86eb0dd55 100644 --- a/swift-sdk/Internal/IterableTaskManagedObject.swift +++ b/swift-sdk/Internal/IterableTaskManagedObject.swift @@ -21,6 +21,7 @@ extension IterableTaskManagedObject { @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 From 58551b84cbe05614be4526e9c0842ae43d035b99 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 15 Sep 2020 14:01:19 +0530 Subject: [PATCH 60/76] Remove unneeded `createTask` method. --- .../IterableCoreDataPersistence.swift | 6 +----- swift-sdk/Internal/IterablePersistence.swift | 3 --- .../offline-events-tests/TasksCRUDTests.swift | 19 ++++++++++++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index eb7fa82d3..fa757b4e0 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -94,11 +94,7 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { func delete(task: IterableTask) throws { try deleteTask(withId: task.id) } - - func createTask(id: String, type: IterableTaskType) throws -> IterableTask { - try create(task: IterableTask(id: id, type: type, scheduledAt: dateProvider.currentDate, requestedAt: dateProvider.currentDate)) - } - + func nextTask() throws -> IterableTask? { let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findSortedEntities(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name, diff --git a/swift-sdk/Internal/IterablePersistence.swift b/swift-sdk/Internal/IterablePersistence.swift index 47b523ac0..df46d3f44 100644 --- a/swift-sdk/Internal/IterablePersistence.swift +++ b/swift-sdk/Internal/IterablePersistence.swift @@ -29,9 +29,6 @@ protocol IterablePersistenceContext { func delete(task: IterableTask) throws - @discardableResult - func createTask(id: String, type: IterableTaskType) throws -> IterableTask - func findTask(withId id: String) throws -> IterableTask? func deleteTask(withId id: String) throws diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index c83afc2d0..01d63b000 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -11,7 +11,7 @@ class TasksCRUDTests: XCTestCase { func testCreate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - let task = try context.createTask(id: taskId, type: .apiCall) + let task = try createTask(context: context, id: taskId, type: .apiCall) try context.save() XCTAssertEqual(task.id, taskId) XCTAssertEqual(task.type, .apiCall) @@ -25,7 +25,7 @@ class TasksCRUDTests: XCTestCase { func testUpdate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - let task = try context.createTask(id: taskId, type: .apiCall) + let task = try createTask(context: context, id: taskId, type: .apiCall) try context.save() let attempts = 2 @@ -67,7 +67,7 @@ class TasksCRUDTests: XCTestCase { func testDelete() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - try context.createTask(id: taskId, type: .apiCall) + try createTask(context: context, id: taskId, type: .apiCall) try context.save() let newContext = persistenceProvider.mainQueueContext() @@ -128,8 +128,8 @@ class TasksCRUDTests: XCTestCase { let tasks = try context.findAllTasks() XCTAssertEqual(tasks.count, 0) - try context.createTask(id: IterableUtil.generateUUID(), type: .apiCall) - try context.createTask(id: IterableUtil.generateUUID(), type: .apiCall) + 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() @@ -139,6 +139,15 @@ class TasksCRUDTests: XCTestCase { try context.save() } + @discardableResult + private func createTask(context: IterablePersistenceContext, id: String, type: IterableTaskType) throws -> IterableTask { + let template = IterableTask(id: id, + type: type, + scheduledAt: dateProvider.currentDate, + requestedAt: dateProvider.currentDate) + return try context.create(task: template) + } + private let dateProvider = MockDateProvider() private lazy var persistenceProvider: IterablePersistenceContextProvider = { From aaf990c0aca66cb245a3848596334daa70b106eb Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 15 Sep 2020 14:36:58 +0530 Subject: [PATCH 61/76] Ensure name gets populated when scheduling an API call task. --- swift-sdk.xcodeproj/project.pbxproj | 4 + .../Internal/IterableAPICallRequest.swift | 9 +++ .../Internal/IterableTaskScheduler.swift | 1 + swift-sdk/Internal/PersistenceHelper.swift | 2 + .../IterableDataModel.xcdatamodel/contents | 5 +- .../TaskSchedulerTests.swift | 73 +++++++++++++++++++ .../offline-events-tests/TasksCRUDTests.swift | 10 ++- 7 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/offline-events-tests/TaskSchedulerTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 80992edd8..432281a86 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 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 */; }; @@ -408,6 +409,7 @@ 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 = ""; }; @@ -1155,6 +1157,7 @@ ACCF274B24F40C85004862D5 /* RequestProcessorTests.swift */, AC67AF972507481200C1E974 /* NetworkConnectivityCheckerTests.swift */, ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */, + AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */, ); path = "offline-events-tests"; sourceTree = ""; @@ -1834,6 +1837,7 @@ 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 */, diff --git a/swift-sdk/Internal/IterableAPICallRequest.swift b/swift-sdk/Internal/IterableAPICallRequest.swift index 199698245..1674a234e 100644 --- a/swift-sdk/Internal/IterableAPICallRequest.swift +++ b/swift-sdk/Internal/IterableAPICallRequest.swift @@ -23,6 +23,15 @@ struct IterableAPICallRequest { } } + 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, diff --git a/swift-sdk/Internal/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift index bd45b8968..2baee1e57 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -24,6 +24,7 @@ class IterableTaskScheduler { let data = try JSONEncoder().encode(apiCallRequest) try persistenceContext.create(task: IterableTask(id: taskId, + name: apiCallRequest.getPath(), type: .apiCall, scheduledAt: scheduledAt ?? dateProvider.currentDate, data: data, diff --git a/swift-sdk/Internal/PersistenceHelper.swift b/swift-sdk/Internal/PersistenceHelper.swift index 1efe685f0..53def6462 100644 --- a/swift-sdk/Internal/PersistenceHelper.swift +++ b/swift-sdk/Internal/PersistenceHelper.swift @@ -8,6 +8,7 @@ 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, @@ -25,6 +26,7 @@ struct PersistenceHelper { 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 diff --git a/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents index 8d1eec63b..718076578 100644 --- a/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents +++ b/swift-sdk/Resources/IterableDataModel.xcdatamodeld/IterableDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -9,6 +9,7 @@ + @@ -23,6 +24,6 @@ - + \ No newline at end of file diff --git a/tests/offline-events-tests/TaskSchedulerTests.swift b/tests/offline-events-tests/TaskSchedulerTests.swift new file mode 100644 index 000000000..2307db13e --- /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) + + 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 index 01d63b000..a08cff425 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -11,10 +11,12 @@ class TasksCRUDTests: XCTestCase { func testCreate() throws { let context = persistenceProvider.newBackgroundContext() let taskId = IterableUtil.generateUUID() - let task = try createTask(context: context, id: taskId, type: .apiCall) + 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)! @@ -140,8 +142,12 @@ class TasksCRUDTests: XCTestCase { } @discardableResult - private func createTask(context: IterablePersistenceContext, id: String, type: IterableTaskType) throws -> IterableTask { + 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) From e7a2f7a1003fec4aede92df0e24dc7985147f852 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 18 Sep 2020 12:52:25 +0530 Subject: [PATCH 62/76] Add TaskRunner to TaskProcessor. --- swift-sdk/Internal/IterableTaskRunner.swift | 4 ++- .../Internal/NetworkConnectivityManager.swift | 2 +- .../Internal/OfflineRequestProcessor.swift | 18 +++++++++++-- .../Internal/OnlineRequestProcessor.swift | 8 ++++++ swift-sdk/Internal/RequestProcessor.swift | 14 +++++++++- .../Internal/RequestProcessorProtocol.swift | 4 +++ .../RequestProcessorTests.swift | 26 +++++++++---------- 7 files changed, 57 insertions(+), 19 deletions(-) diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index ded52ebc3..2e1e5dd9c 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -36,7 +36,7 @@ class IterableTaskRunner: NSObject { selector: #selector(onAppDidEnterBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil) - self.connectivityManager.connectivityChangedCallback = onConnectivityChanged(connected:) + self.connectivityManager.connectivityChangedCallback = { [weak self] in self?.onConnectivityChanged(connected: $0) } } func start() { @@ -50,6 +50,7 @@ class IterableTaskRunner: NSObject { ITBInfo() paused = true timer?.invalidate() + timer = nil connectivityManager.stop() } @@ -75,6 +76,7 @@ class IterableTaskRunner: NSObject { private func runNow() { timer?.invalidate() + timer = nil run() } diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index 424cf0de6..8fa935c06 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -42,7 +42,7 @@ class NetworkConnectivityManager: NSObject { func start() { ITBInfo() - networkMonitor.statusUpdatedCallback = updateStatus + networkMonitor.statusUpdatedCallback = { [weak self] in self?.updateStatus() } networkMonitor.start() startTimer() } diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 31daebb06..8a51c1147 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -12,15 +12,28 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { authManager: IterableInternalAuthManagerProtocol?, endPoint: String, deviceMetadata: DeviceMetadata, - notificationCenter: NotificationCenterProtocol) { + taskRunner: IterableTaskRunner = IterableTaskRunner(), + notificationCenter: NotificationCenterProtocol + ) { self.apiKey = apiKey self.authProvider = authProvider self.authManager = authManager self.endPoint = endPoint self.deviceMetadata = deviceMetadata + self.taskRunner = taskRunner notificationListener = NotificationListener(notificationCenter: notificationCenter) } + func start() { + ITBInfo() + taskRunner.start() + } + + func stop(){ + ITBInfo() + taskRunner.stop() + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, @@ -327,6 +340,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { private let endPoint: String private let deviceMetadata: DeviceMetadata private let notificationListener: NotificationListener + private let taskRunner: IterableTaskRunner private func createRequestCreator(authProvider: AuthProvider) -> RequestCreator { return RequestCreator(apiKey: apiKey, auth: authProvider.auth, deviceMetadata: deviceMetadata) @@ -423,7 +437,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { } } - private var notificationCenter: NotificationCenterProtocol + private let notificationCenter: NotificationCenterProtocol private var pendingTasksMap = [String: Promise]() } } diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index b00605372..7516275ed 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -21,6 +21,14 @@ struct OnlineRequestProcessor: RequestProcessorProtocol { deviceMetadata: deviceMetadata) } + func start() { + ITBInfo() + } + + func stop() { + ITBInfo() + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 027927496..3d6c3a03e 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -26,12 +26,14 @@ struct RequestProcessor: RequestProcessorProtocol { deviceMetadata: DeviceMetadata, networkSession: NetworkSessionProtocol, notificationCenter: NotificationCenterProtocol, - strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { + strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false), + taskRunner: IterableTaskRunner = IterableTaskRunner()) { offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, authProvider: authProvider, authManager: authManager, endPoint: endPoint, deviceMetadata: deviceMetadata, + taskRunner: taskRunner, notificationCenter: notificationCenter) onlineProcessor = OnlineRequestProcessor(apiKey: apiKey, authProvider: authProvider, @@ -42,6 +44,16 @@ struct RequestProcessor: RequestProcessorProtocol { self.strategy = strategy } + func start() { + ITBInfo() + chooseRequestProcessor().start() + } + + func stop() { + ITBInfo() + chooseRequestProcessor().stop() + } + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, diff --git a/swift-sdk/Internal/RequestProcessorProtocol.swift b/swift-sdk/Internal/RequestProcessorProtocol.swift index bcfa042f9..032550add 100644 --- a/swift-sdk/Internal/RequestProcessorProtocol.swift +++ b/swift-sdk/Internal/RequestProcessorProtocol.swift @@ -26,6 +26,10 @@ struct UpdateSubscriptionsInfo { /// `IterableAPIinternal` will delegate all network related calls to this struct. protocol RequestProcessorProtocol { + func start() + + func stop() + @discardableResult func register(registerTokenInfo: RegisterTokenInfo, notificationStateProvider: NotificationStateProviderProtocol, diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index 60ee104eb..d4a729c83 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -674,8 +674,7 @@ class RequestProcessorTests: XCTestCase { path: path, bodyDict: bodyDict, expectation: expectation1) - waitForTaskRunner(networkSession: networkSession, - notificationCenter: notificationCenter, + waitForTaskRunner(requestProcessor: requestProcessor, expectation: expectation1) } @@ -698,22 +697,25 @@ class RequestProcessorTests: XCTestCase { path: path, bodyDict: bodyDict, expectation: expectation1) - waitForTaskRunner(networkSession: networkSession, - notificationCenter: notificationCenter, + waitForTaskRunner(requestProcessor: requestProcessor, expectation: expectation1) } private func createRequestProcessor(networkSession: NetworkSessionProtocol, notificationCenter: NotificationCenterProtocol, selectOffline: Bool) -> RequestProcessorProtocol { - RequestProcessor(apiKey: "zee-api-key", + let taskRunner = IterableTaskRunner(networkSession: networkSession, + notificationCenter: notificationCenter, + timeInterval: 0.5) + return RequestProcessor(apiKey: "zee-api-key", authProvider: self, authManager: nil, endPoint: Endpoint.api, deviceMetadata: deviceMetadata, networkSession: networkSession, notificationCenter: notificationCenter, - strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline)) + strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline), + taskRunner: taskRunner) } private func processRequestWithSuccess(request: () -> Future, @@ -749,17 +751,13 @@ class RequestProcessorTests: XCTestCase { } } - private func waitForTaskRunner(networkSession: NetworkSessionProtocol, - notificationCenter: NotificationCenterProtocol, + private func waitForTaskRunner(requestProcessor: RequestProcessorProtocol, expectation: XCTestExpectation) { - let taskRunner = IterableTaskRunner(networkSession: networkSession, - notificationCenter: notificationCenter, - timeInterval: 0.5) - taskRunner.start() + requestProcessor.start() wait(for: [expectation], timeout: 15.0) - taskRunner.stop() + requestProcessor.stop() } - + struct Exp { let successExpectation: XCTestExpectation let onSuccess: OnSuccessHandler From ab7c7a8b1f897edfa2be56d6262054d8031f8c4b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 18 Sep 2020 14:10:55 +0530 Subject: [PATCH 63/76] 1. Add resources to swift package manager. 2. Add offline mode to config --- Package.swift | 7 +++++-- swift-sdk/Internal/IterableAPIInternal.swift | 3 ++- swift-sdk/IterableConfig.swift | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2c3daaddc..04fa38ba1 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: [ + .copy("Resources"), + ]), .target(name: "IterableAppExtensions", path: "notification-extension"), ] diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 7c3df642b..f71aa8ade 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -392,7 +392,8 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { endPoint: config.apiEndpoint, deviceMetadata: deviceMetadata, networkSession: networkSession, - notificationCenter: dependencyContainer.notificationCenter) + notificationCenter: dependencyContainer.notificationCenter, + strategy: DefaultRequestProcessorStrategy(selectOffline: config.enableOfflineMode)) } else { return OnlineRequestProcessor(apiKey: apiKey, authProvider: self, diff --git a/swift-sdk/IterableConfig.swift b/swift-sdk/IterableConfig.swift index 67dbfc2b6..dee52ab95 100644 --- a/swift-sdk/IterableConfig.swift +++ b/swift-sdk/IterableConfig.swift @@ -120,6 +120,10 @@ public class IterableConfig: NSObject { /// How many seconds to wait before showing the next in-app, if there are more than one present public var inAppDisplayInterval: Double = 30.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 internal var linksEndpoint = Endpoint.links From ffd12407e81e156cbda8dc8393b0dd3928d4c03b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 18 Sep 2020 15:59:10 +0530 Subject: [PATCH 64/76] Initialize core data using bundle resource. --- Package.swift | 2 +- swift-sdk/Internal/IterableCoreDataPersistence.swift | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 04fa38ba1..adfd65f05 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .target(name: "IterableSDK", path: "swift-sdk", resources: [ - .copy("Resources"), + .process("Resources"), ]), .target(name: "IterableAppExtensions", path: "notification-extension"), diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index fa757b4e0..2cdc14e57 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -8,6 +8,7 @@ import Foundation enum PersistenceConst { static let dataModelFileName = "IterableDataModel" + static let dataModelExtension = "momd" enum Entity { enum Task { @@ -24,7 +25,11 @@ enum PersistenceConst { @available(iOS 10.0, *) class PersistentContainer: NSPersistentContainer { static let shared: PersistentContainer = { - let container = PersistentContainer(name: PersistenceConst.dataModelFileName) + // TODO: @tqm remove force unwrapping + let url = Bundle(for: PersistentContainer.self).url(forResource: PersistenceConst.dataModelFileName, withExtension: PersistenceConst.dataModelExtension)! + let managedObjectModel = NSManagedObjectModel(contentsOf: url)! + + let container = PersistentContainer(name: PersistenceConst.dataModelFileName, managedObjectModel: managedObjectModel) container.loadPersistentStores { desc, error in if let error = error { fatalError("Unresolved error \(error)") From e85591b8f73f9b4371a37251b1bdae3840ce3254 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 18 Sep 2020 16:23:45 +0530 Subject: [PATCH 65/76] Use bundle loading that works for package manager. --- .../IterableCoreDataPersistence.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 2cdc14e57..80c63d949 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -22,11 +22,37 @@ enum PersistenceConst { } } +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 = { // TODO: @tqm remove force unwrapping - let url = Bundle(for: PersistentContainer.self).url(forResource: PersistenceConst.dataModelFileName, withExtension: PersistenceConst.dataModelExtension)! + let url = Bundle.current.url(forResource: PersistenceConst.dataModelFileName, withExtension: PersistenceConst.dataModelExtension)! let managedObjectModel = NSManagedObjectModel(contentsOf: url)! let container = PersistentContainer(name: PersistenceConst.dataModelFileName, managedObjectModel: managedObjectModel) From 206a4b12ceda5772e8711c7f8d38c35ef0dde96b Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Fri, 18 Sep 2020 21:39:04 +0530 Subject: [PATCH 66/76] Start the request processor. --- swift-sdk/Internal/IterableAPIInternal.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index f71aa8ade..7f192ae70 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -539,6 +539,8 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { handle(launchOptions: launchOptions) + requestProcessor.start() + return inAppManager.start() } From 9e69e591f8c1a03f0f91572167768bc248433453 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 21 Sep 2020 20:32:39 +0530 Subject: [PATCH 67/76] Minor comments etc. --- swift-sdk/Internal/IterableCoreDataPersistence.swift | 2 ++ swift-sdk/Internal/IterableTaskRunner.swift | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 80c63d949..28a33b16a 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -22,6 +22,8 @@ enum PersistenceConst { } } +/// `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 { diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 2e1e5dd9c..db9e79252 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -164,7 +164,7 @@ class IterableTaskRunner: NSObject { @discardableResult private func execute(task: IterableTask) -> Future { - ITBInfo("Executing taskId: \(task.id)") + ITBInfo("Executing taskId: \(task.id), name: \(task.name ?? "nil")") guard task.processing == false else { return Promise(value: .processing) } @@ -212,6 +212,9 @@ class IterableTaskRunner: NSObject { } 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)") From 5021db7c0a1863975637537670a010867c617143 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 23 Sep 2020 19:07:42 +0530 Subject: [PATCH 68/76] Use dependency container to create request processor. --- swift-sdk/Internal/DependencyContainer.swift | 40 ++++++++++++++++ swift-sdk/Internal/IterableAPIInternal.swift | 24 +++------- swift-sdk/Internal/IterableTaskRunner.swift | 1 - swift-sdk/Internal/RequestProcessor.swift | 42 +++++++--------- .../RequestProcessorTests.swift | 48 +++++++++++-------- 5 files changed, 91 insertions(+), 64 deletions(-) diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index 7bfdb5021..7b3919ef7 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -47,6 +47,46 @@ 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: { + OnlineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) }, + offlineCreator: { + OfflineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + deviceMetadata: deviceMetadata, + taskRunner: createTaskRunner(), + notificationCenter: notificationCenter) }, + strategy: DefaultRequestProcessorStrategy(selectOffline: config.enableOfflineMode)) + } else { + return OnlineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endPoint: config.apiEndpoint, + networkSession: networkSession, + deviceMetadata: deviceMetadata) + } + } + + @available(iOS 10.0, *) + private func createTaskRunner() -> IterableTaskRunner { + IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: CoreDataPersistenceContextProvider(dateProvider: dateProvider), + notificationCenter: notificationCenter, + connectivityManager: NetworkConnectivityManager()) + } } struct DependencyContainer: DependencyContainerProtocol { diff --git a/swift-sdk/Internal/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index b7ed9d081..c8ed4c03e 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -387,24 +387,12 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deviceMetadata: deviceMetadata) }() - lazy var requestProcessor: RequestProcessorProtocol = { - if #available(iOS 10.0, *) { - return RequestProcessor(apiKey: apiKey, - authProvider: self, - authManager: authManager, - endPoint: config.apiEndpoint, - deviceMetadata: deviceMetadata, - networkSession: networkSession, - notificationCenter: dependencyContainer.notificationCenter, - strategy: DefaultRequestProcessorStrategy(selectOffline: config.enableOfflineMode)) - } else { - return OnlineRequestProcessor(apiKey: apiKey, - authProvider: self, - authManager: authManager, - endPoint: config.apiEndpoint, - networkSession: networkSession, - deviceMetadata: deviceMetadata) - } + private lazy var requestProcessor: RequestProcessorProtocol = { + dependencyContainer.createRequestProcessor(apiKey: apiKey, + config: config, + authProvider: self, + authManager: authManager, + deviceMetadata: deviceMetadata) }() private var deviceAttributes = [String: String]() diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index db9e79252..1a3f1c2d9 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -9,7 +9,6 @@ import UIKit @available(iOS 10.0, *) class IterableTaskRunner: NSObject { - // TODO: @tqm Move to `DependencyContainer` after we remove iOS 9 support init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), notificationCenter: NotificationCenterProtocol = NotificationCenter.default, diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 3d6c3a03e..8af9575a7 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -18,29 +18,12 @@ struct DefaultRequestProcessorStrategy: RequestProcessorStrategy { } @available(iOS 10.0, *) -struct RequestProcessor: RequestProcessorProtocol { - init(apiKey: String, - authProvider: AuthProvider, - authManager: IterableInternalAuthManagerProtocol?, - endPoint: String, - deviceMetadata: DeviceMetadata, - networkSession: NetworkSessionProtocol, - notificationCenter: NotificationCenterProtocol, - strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false), - taskRunner: IterableTaskRunner = IterableTaskRunner()) { - offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, - authProvider: authProvider, - authManager: authManager, - endPoint: endPoint, - deviceMetadata: deviceMetadata, - taskRunner: taskRunner, - notificationCenter: notificationCenter) - onlineProcessor = OnlineRequestProcessor(apiKey: apiKey, - authProvider: authProvider, - authManager: authManager, - endPoint: endPoint, - networkSession: networkSession, - deviceMetadata: deviceMetadata) +class RequestProcessor: RequestProcessorProtocol { + init(onlineCreator: @escaping () -> OnlineRequestProcessor, + offlineCreator: @escaping () -> OfflineRequestProcessor, + strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { + self.onlineCreator = onlineCreator + self.offlineCreator = offlineCreator self.strategy = strategy } @@ -260,9 +243,18 @@ struct RequestProcessor: RequestProcessorProtocol { onFailure: onFailure) } + private let onlineCreator: () -> OnlineRequestProcessor + private let offlineCreator: () -> OfflineRequestProcessor + private let strategy: RequestProcessorStrategy - private let offlineProcessor: OfflineRequestProcessor - private let onlineProcessor: OnlineRequestProcessor + + private lazy var offlineProcessor: OfflineRequestProcessor = { + offlineCreator() + }() + + private lazy var onlineProcessor: OnlineRequestProcessor = { + onlineCreator() + }() private func chooseRequestProcessor() -> RequestProcessorProtocol { strategy.chooseOfflineProcessor ? offlineProcessor: onlineProcessor diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index d4a729c83..629426f44 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -299,7 +299,7 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com", "messageId": messageId, "inboxSessionId": inboxSessionId, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, "messageContext": [ "location": "in-app", "saveToInbox": false, @@ -333,7 +333,7 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com", "messageId": messageId, "inboxSessionId": inboxSessionId, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, "clickedUrl": clickedUrl, "messageContext": [ "location": "inbox", @@ -370,7 +370,7 @@ class RequestProcessorTests: XCTestCase { "email": "user@example.com", "messageId": messageId, "inboxSessionId": inboxSessionId, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, "clickedUrl": clickedUrl, "messageContext": [ "location": "inbox", @@ -426,7 +426,7 @@ class RequestProcessorTests: XCTestCase { "endTotalMessageCount": inboxSession.endTotalMessageCount, "endUnreadMessageCount": inboxSession.endUnreadMessageCount, "impressions": impressions.compactMap { $0.asDictionary() }, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, ] let expectations = createExpectations(description: #function) @@ -456,7 +456,7 @@ class RequestProcessorTests: XCTestCase { "saveToInbox": false, "silentInbox": false, ], - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, ] let expectations = createExpectations(description: #function) @@ -513,7 +513,7 @@ class RequestProcessorTests: XCTestCase { "silentInbox": false, ], "deleteAction": "delete-button", - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, ] let expectations = createExpectations(description: #function) @@ -539,7 +539,7 @@ class RequestProcessorTests: XCTestCase { let bodyDict: [String: Any] = [ "email": "user@example.com", "messageId": messageId, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, "messageContext": [ "location": "in-app", "saveToInbox": false, @@ -568,7 +568,7 @@ class RequestProcessorTests: XCTestCase { let bodyDict: [String: Any] = [ "email": "user@example.com", "messageId": messageId, - "deviceInfo": deviceMetadata.asDictionary()!, + "deviceInfo": Self.deviceMetadata.asDictionary()!, "clickedUrl": clickedUrl, "messageContext": [ "location": "in-app", @@ -707,15 +707,23 @@ class RequestProcessorTests: XCTestCase { let taskRunner = IterableTaskRunner(networkSession: networkSession, notificationCenter: notificationCenter, timeInterval: 0.5) - return RequestProcessor(apiKey: "zee-api-key", - authProvider: self, - authManager: nil, - endPoint: Endpoint.api, - deviceMetadata: deviceMetadata, - networkSession: networkSession, - notificationCenter: notificationCenter, - strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline), - taskRunner: taskRunner) + + 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, + taskRunner: taskRunner, + notificationCenter: notificationCenter) }, + strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline)) } private func processRequestWithSuccess(request: () -> Future, @@ -792,9 +800,9 @@ class RequestProcessorTests: XCTestCase { return (expectation: expectation1, onFailure: onFailure) } - private let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), - platform: JsonValue.iOS.jsonStringValue, - appPackageName: Bundle.main.appPackageName ?? "") + private static let deviceMetadata = DeviceMetadata(deviceId: IterableUtil.generateUUID(), + platform: JsonValue.iOS.jsonStringValue, + appPackageName: Bundle.main.appPackageName ?? "") private let dateProvider = MockDateProvider() From deadd055ed55e9dc7c8621379aa8d088e8d2b514 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 28 Sep 2020 12:04:43 +0530 Subject: [PATCH 69/76] Fix dependency injection for IterableSDK. Handle failure when creating CoreDataPersistenceContextProvider. --- swift-sdk/Internal/DependencyContainer.swift | 29 ++++++++++++++++--- .../IterableCoreDataPersistence.swift | 24 ++++++++++----- swift-sdk/Internal/IterableTaskRunner.swift | 2 +- .../Internal/IterableTaskScheduler.swift | 3 +- .../Internal/OfflineRequestProcessor.swift | 17 ++++++----- swift-sdk/Internal/RequestProcessor.swift | 15 +++++++--- .../RequestProcessorTests.swift | 7 ++++- .../TaskProcessorTests.swift | 2 +- .../TaskRunnerTests.swift | 8 ++++- .../TaskSchedulerTests.swift | 2 +- .../offline-events-tests/TasksCRUDTests.swift | 2 +- 11 files changed, 81 insertions(+), 30 deletions(-) diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index 7b3919ef7..65452f343 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 { @@ -62,12 +63,17 @@ extension DependencyContainerProtocol { networkSession: networkSession, deviceMetadata: deviceMetadata) }, offlineCreator: { - OfflineRequestProcessor(apiKey: apiKey, + guard let persistenceContextProvider = createPersistenceContextProvider() else { + return nil + } + + return OfflineRequestProcessor(apiKey: apiKey, authProvider: authProvider, authManager: authManager, endPoint: config.apiEndpoint, deviceMetadata: deviceMetadata, - taskRunner: createTaskRunner(), + taskScheduler: createTaskScheduler(persistenceContextProvider: persistenceContextProvider), + taskRunner: createTaskRunner(persistenceContextProvider: persistenceContextProvider), notificationCenter: notificationCenter) }, strategy: DefaultRequestProcessorStrategy(selectOffline: config.enableOfflineMode)) } else { @@ -80,10 +86,25 @@ extension DependencyContainerProtocol { } } + 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() -> IterableTaskRunner { + private func createTaskRunner(persistenceContextProvider: IterablePersistenceContextProvider) -> IterableTaskRunner { IterableTaskRunner(networkSession: networkSession, - persistenceContextProvider: CoreDataPersistenceContextProvider(dateProvider: dateProvider), + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, connectivityManager: NetworkConnectivityManager()) } diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 28a33b16a..010c0be05 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -52,10 +52,15 @@ extension Foundation.Bundle { @available(iOS 10.0, *) class PersistentContainer: NSPersistentContainer { - static let shared: PersistentContainer = { - // TODO: @tqm remove force unwrapping - let url = Bundle.current.url(forResource: PersistenceConst.dataModelFileName, withExtension: PersistenceConst.dataModelExtension)! - let managedObjectModel = NSManagedObjectModel(contentsOf: url)! + 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 @@ -82,18 +87,23 @@ class PersistentContainer: NSPersistentContainer { @available(iOS 10.0, *) struct CoreDataPersistenceContextProvider: IterablePersistenceContextProvider { - init(dateProvider: DateProviderProtocol = SystemDateProvider()) { + init?(dateProvider: DateProviderProtocol = SystemDateProvider()) { + guard let persistentContainer = PersistentContainer.shared else { + return nil + } + self.persistentContainer = persistentContainer self.dateProvider = dateProvider } func newBackgroundContext() -> IterablePersistenceContext { - CoreDataPersistenceContext(managedObjectContext: PersistentContainer.shared.newBackgroundContext(), dateProvider: dateProvider) + return CoreDataPersistenceContext(managedObjectContext: persistentContainer.newBackgroundContext(), dateProvider: dateProvider) } func mainQueueContext() -> IterablePersistenceContext { - CoreDataPersistenceContext(managedObjectContext: PersistentContainer.shared.viewContext, dateProvider: dateProvider) + return CoreDataPersistenceContext(managedObjectContext: persistentContainer.viewContext, dateProvider: dateProvider) } + private let persistentContainer: PersistentContainer private let dateProvider: DateProviderProtocol } diff --git a/swift-sdk/Internal/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 1a3f1c2d9..31a8df927 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -10,7 +10,7 @@ import UIKit @available(iOS 10.0, *) class IterableTaskRunner: NSObject { init(networkSession: NetworkSessionProtocol = URLSession(configuration: .default), - persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), + persistenceContextProvider: IterablePersistenceContextProvider, notificationCenter: NotificationCenterProtocol = NotificationCenter.default, timeInterval: TimeInterval = 1.0 * 60, connectivityManager: NetworkConnectivityManager = NetworkConnectivityManager()) { diff --git a/swift-sdk/Internal/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift index 2baee1e57..3413baf41 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -7,8 +7,7 @@ import Foundation @available(iOS 10.0, *) class IterableTaskScheduler { - // TODO: @tqm Move to `DependencyContainer` after we remove iOS 9 support - init(persistenceContextProvider: IterablePersistenceContextProvider = CoreDataPersistenceContextProvider(), + init(persistenceContextProvider: IterablePersistenceContextProvider, notificationCenter: NotificationCenterProtocol = NotificationCenter.default, dateProvider: DateProviderProtocol = SystemDateProvider()) { self.persistenceContextProvider = persistenceContextProvider diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index 8a51c1147..ad85bab47 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -12,7 +12,8 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { authManager: IterableInternalAuthManagerProtocol?, endPoint: String, deviceMetadata: DeviceMetadata, - taskRunner: IterableTaskRunner = IterableTaskRunner(), + taskScheduler: IterableTaskScheduler, + taskRunner: IterableTaskRunner, notificationCenter: NotificationCenterProtocol ) { self.apiKey = apiKey @@ -20,6 +21,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { self.authManager = authManager self.endPoint = endPoint self.deviceMetadata = deviceMetadata + self.taskScheduler = taskScheduler self.taskRunner = taskRunner notificationListener = NotificationListener(notificationCenter: notificationCenter) } @@ -340,6 +342,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { 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 { @@ -366,14 +369,14 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { iterableRequest: iterableRequest) do { - let taskId = try IterableTaskScheduler().schedule(apiCallRequest: apiCallRequest, - context: IterableTaskContext(blocking: true)) + let taskId = try taskScheduler.schedule(apiCallRequest: apiCallRequest, + context: IterableTaskContext(blocking: true)) let result = notificationListener.futureFromTask(withTaskId: taskId) return RequestProcessorUtil.apply(successHandler: onSuccess, - andFailureHandler: onFailure, - andAuthManager: authManager, - toResult: result, - withIdentifier: identifier) + andFailureHandler: onFailure, + andAuthManager: authManager, + toResult: result, + withIdentifier: identifier) } catch let error { ITBError(error.localizedDescription) return SendRequestError.createErroredFuture(reason: error.localizedDescription) diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 8af9575a7..62e5bde67 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -20,7 +20,7 @@ struct DefaultRequestProcessorStrategy: RequestProcessorStrategy { @available(iOS 10.0, *) class RequestProcessor: RequestProcessorProtocol { init(onlineCreator: @escaping () -> OnlineRequestProcessor, - offlineCreator: @escaping () -> OfflineRequestProcessor, + offlineCreator: @escaping () -> OfflineRequestProcessor?, strategy: RequestProcessorStrategy = DefaultRequestProcessorStrategy(selectOffline: false)) { self.onlineCreator = onlineCreator self.offlineCreator = offlineCreator @@ -244,11 +244,11 @@ class RequestProcessor: RequestProcessorProtocol { } private let onlineCreator: () -> OnlineRequestProcessor - private let offlineCreator: () -> OfflineRequestProcessor + private let offlineCreator: () -> OfflineRequestProcessor? private let strategy: RequestProcessorStrategy - private lazy var offlineProcessor: OfflineRequestProcessor = { + private lazy var offlineProcessor: OfflineRequestProcessor? = { offlineCreator() }() @@ -257,6 +257,13 @@ class RequestProcessor: RequestProcessorProtocol { }() private func chooseRequestProcessor() -> RequestProcessorProtocol { - strategy.chooseOfflineProcessor ? offlineProcessor: onlineProcessor + if strategy.chooseOfflineProcessor { + if let offlineProcessor = self.offlineProcessor { + return offlineProcessor + } + return onlineProcessor + } else { + return onlineProcessor + } } } diff --git a/tests/offline-events-tests/RequestProcessorTests.swift b/tests/offline-events-tests/RequestProcessorTests.swift index 629426f44..f8a0b90c7 100644 --- a/tests/offline-events-tests/RequestProcessorTests.swift +++ b/tests/offline-events-tests/RequestProcessorTests.swift @@ -704,7 +704,11 @@ class RequestProcessorTests: XCTestCase { 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) @@ -721,6 +725,7 @@ class RequestProcessorTests: XCTestCase { authManager: nil, endPoint: Endpoint.api, deviceMetadata: Self.deviceMetadata, + taskScheduler: taskScheduler, taskRunner: taskRunner, notificationCenter: notificationCenter) }, strategy: DefaultRequestProcessorStrategy(selectOffline: selectOffline)) @@ -807,7 +812,7 @@ class RequestProcessorTests: XCTestCase { private let dateProvider = MockDateProvider() private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! return provider }() } diff --git a/tests/offline-events-tests/TaskProcessorTests.swift b/tests/offline-events-tests/TaskProcessorTests.swift index 03673b5f3..10f5fb8f8 100644 --- a/tests/offline-events-tests/TaskProcessorTests.swift +++ b/tests/offline-events-tests/TaskProcessorTests.swift @@ -183,7 +183,7 @@ class TaskProcessorTests: XCTestCase { appPackageName: Bundle.main.appPackageName ?? "") private lazy var persistenceProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider() + 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 index 040fb3086..95f8e0ad9 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -35,6 +35,7 @@ class TaskRunnerTests: XCTestCase { } let taskRunner = IterableTaskRunner(networkSession: MockNetworkSession(), + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, timeInterval: 0.5) taskRunner.start() @@ -65,6 +66,7 @@ class TaskRunnerTests: XCTestCase { } let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, timeInterval: 1.0) taskRunner.start() @@ -100,6 +102,7 @@ class TaskRunnerTests: XCTestCase { } let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, timeInterval: 0.5) taskRunner.start() @@ -125,6 +128,7 @@ class TaskRunnerTests: XCTestCase { notificationCenter: notificationCenter) let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, connectivityManager: manager) taskRunner.start() @@ -151,6 +155,7 @@ class TaskRunnerTests: XCTestCase { notificationCenter: notificationCenter) let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, connectivityManager: manager) taskRunner.start() @@ -183,6 +188,7 @@ class TaskRunnerTests: XCTestCase { notificationCenter: notificationCenter) let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, timeInterval: 0.5, connectivityManager: manager) @@ -259,7 +265,7 @@ class TaskRunnerTests: XCTestCase { appPackageName: Bundle.main.appPackageName ?? "") private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! return provider }() diff --git a/tests/offline-events-tests/TaskSchedulerTests.swift b/tests/offline-events-tests/TaskSchedulerTests.swift index 2307db13e..e1daec59f 100644 --- a/tests/offline-events-tests/TaskSchedulerTests.swift +++ b/tests/offline-events-tests/TaskSchedulerTests.swift @@ -59,7 +59,7 @@ class TaskSchedulerTests: XCTestCase { appPackageName: Bundle.main.appPackageName ?? "") private lazy var persistenceContextProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! return provider }() diff --git a/tests/offline-events-tests/TasksCRUDTests.swift b/tests/offline-events-tests/TasksCRUDTests.swift index a08cff425..2f5c19d35 100644 --- a/tests/offline-events-tests/TasksCRUDTests.swift +++ b/tests/offline-events-tests/TasksCRUDTests.swift @@ -157,7 +157,7 @@ class TasksCRUDTests: XCTestCase { private let dateProvider = MockDateProvider() private lazy var persistenceProvider: IterablePersistenceContextProvider = { - let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider) + let provider = CoreDataPersistenceContextProvider(dateProvider: dateProvider)! try! provider.mainQueueContext().deleteAllTasks() try! provider.mainQueueContext().save() return provider From 2db31bc5f8be2c402f8b58b336cfdbcb572650c6 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Mon, 28 Sep 2020 13:25:38 +0530 Subject: [PATCH 70/76] Fix TaskScheduler `schedule` method to return `Result`. --- .../Internal/IterableTaskScheduler.swift | 31 ++++++++++--------- .../Internal/OfflineRequestProcessor.swift | 8 ++--- .../TaskRunnerTests.swift | 2 +- .../TaskSchedulerTests.swift | 2 +- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/swift-sdk/Internal/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift index 3413baf41..74667a5e2 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -17,22 +17,25 @@ class IterableTaskScheduler { func schedule(apiCallRequest: IterableAPICallRequest, context: IterableTaskContext = IterableTaskContext(blocking: true), - scheduledAt: Date? = nil) throws -> String { + scheduledAt: Date? = nil) -> Result { ITBInfo() let taskId = IterableUtil.generateUUID() - 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) - - return taskId + 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 diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index ad85bab47..aad495b9f 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -367,17 +367,15 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { auth: authProvider.auth, deviceMetadata: deviceMetadata, iterableRequest: iterableRequest) - - do { - let taskId = try taskScheduler.schedule(apiCallRequest: apiCallRequest, - context: IterableTaskContext(blocking: true)) + 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) - } catch let error { + case .failure(let error): ITBError(error.localizedDescription) return SendRequestError.createErroredFuture(reason: error.localizedDescription) } diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index 95f8e0ad9..e3c6f62cc 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -231,7 +231,7 @@ class TaskRunnerTests: XCTestCase { return try IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, - dateProvider: dateProvider).schedule(apiCallRequest: apiCallRequest) + dateProvider: dateProvider).schedule(apiCallRequest: apiCallRequest).get() } private func verifyNoTaskIsExecuted(_ notificationCenter: MockNotificationCenter, forInterval interval: TimeInterval) { diff --git a/tests/offline-events-tests/TaskSchedulerTests.swift b/tests/offline-events-tests/TaskSchedulerTests.swift index e1daec59f..aac8363d8 100644 --- a/tests/offline-events-tests/TaskSchedulerTests.swift +++ b/tests/offline-events-tests/TaskSchedulerTests.swift @@ -45,7 +45,7 @@ class TaskSchedulerTests: XCTestCase { let scheduler = IterableTaskScheduler(persistenceContextProvider: persistenceContextProvider, notificationCenter: notificationCenter, dateProvider: dateProvider) - let taskId = try scheduler.schedule(apiCallRequest: apiCallRequest) + let taskId = try scheduler.schedule(apiCallRequest: apiCallRequest).get() wait(for: [expectation1], timeout: 10.0) From 4c096acc43df9621fab5bb02375f5536db60610e Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Tue, 29 Sep 2020 14:39:55 +0530 Subject: [PATCH 71/76] IGNORE: Fake commit to create a separate branch. --- .../TaskRunnerTests.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index e3c6f62cc..cfa4d07cd 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -212,6 +212,51 @@ class TaskRunnerTests: XCTestCase { verifyTaskIsExecuted(notificationCenter, withinInterval: 10.0) taskRunner.stop() } + + func ignore_testMultiThreading() throws { + let expectation1 = expectation(description: #function) + + struct LongRunningNetworkSession: NetworkSessionProtocol { + var statusCode = 200 + var interval: TimeInterval + + func makeRequest(_ request: URLRequest, completionHandler: @escaping CompletionHandler) { + let response = HTTPURLResponse(url: request.url!, statusCode: self.statusCode, httpVersion: "HTTP/1.1", headerFields: [:]) + DispatchQueue.global().asyncAfter(deadline: .now() + interval) { + completionHandler([:].toJsonData(), response, nil) + } + } + + func makeDataRequest(with url: URL, completionHandler: @escaping CompletionHandler) { + fatalError() + } + + func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { + fatalError() + } + + + } + + let networkSession = LongRunningNetworkSession(interval: 1.0) + let notificationCenter = MockNotificationCenter() + let taskRunner = IterableTaskRunner(networkSession: networkSession, + persistenceContextProvider: persistenceContextProvider, + notificationCenter: notificationCenter, + timeInterval: 0.001) + taskRunner.start() + + var taskIds = [String]() + 2.times { + let taskId = try! scheduleSampleTask(notificationCenter: notificationCenter) + taskIds.append(taskId) +// Thread.sleep(forTimeInterval: 1.0) +// taskRunner.start() + } + + wait(for: [expectation1], timeout: 2.0) + taskRunner.stop() + } private func scheduleSampleTask(notificationCenter: NotificationCenterProtocol) throws -> String { let apiKey = "zee-api-key" From 16894993f17d15f3ca3d37bafb5ec9d1e6269256 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 30 Sep 2020 19:54:22 +0530 Subject: [PATCH 72/76] Fix memory leaks. Use weak references. --- swift-sdk/Internal/ApiClient.swift | 2 +- swift-sdk/Internal/DependencyContainer.swift | 6 +++--- swift-sdk/Internal/InAppInternal.swift | 2 +- swift-sdk/Internal/IterableAPIInternal.swift | 1 + .../IterableAppIntegrationInternal.swift | 14 +++++++------- swift-sdk/Internal/IterableTaskRunner.swift | 17 +++++++++++------ .../Internal/NetworkConnectivityManager.swift | 2 +- swift-sdk/Internal/NetworkMonitor.swift | 4 +++- .../Internal/OfflineRequestProcessor.swift | 5 ++++- swift-sdk/Internal/OnlineRequestProcessor.swift | 2 +- swift-sdk/Internal/RequestProcessor.swift | 5 +++++ 11 files changed, 38 insertions(+), 22 deletions(-) diff --git a/swift-sdk/Internal/ApiClient.swift b/swift-sdk/Internal/ApiClient.swift index 0674f7e7a..d8c8aeed7 100644 --- a/swift-sdk/Internal/ApiClient.swift +++ b/swift-sdk/Internal/ApiClient.swift @@ -15,7 +15,7 @@ struct DeviceMetadata: Codable { class ApiClient { init(apiKey: String, - authProvider: AuthProvider, + authProvider: AuthProvider?, endPoint: String, networkSession: NetworkSessionProtocol, deviceMetadata: DeviceMetadata) { diff --git a/swift-sdk/Internal/DependencyContainer.swift b/swift-sdk/Internal/DependencyContainer.swift index a5b412644..48001d04c 100644 --- a/swift-sdk/Internal/DependencyContainer.swift +++ b/swift-sdk/Internal/DependencyContainer.swift @@ -51,18 +51,18 @@ extension DependencyContainerProtocol { func createRequestProcessor(apiKey: String, config: IterableConfig, - authProvider: AuthProvider, + authProvider: AuthProvider?, authManager: IterableInternalAuthManagerProtocol, deviceMetadata: DeviceMetadata) -> RequestProcessorProtocol { if #available(iOS 10.0, *) { - return RequestProcessor(onlineCreator: { + return RequestProcessor(onlineCreator: { [weak authProvider] in OnlineRequestProcessor(apiKey: apiKey, authProvider: authProvider, authManager: authManager, endPoint: config.apiEndpoint, networkSession: networkSession, deviceMetadata: deviceMetadata) }, - offlineCreator: { + offlineCreator: { [weak authProvider] in guard let persistenceContextProvider = createPersistenceContextProvider() else { return nil } 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/IterableAPIInternal.swift b/swift-sdk/Internal/IterableAPIInternal.swift index 01a749f74..f656231b8 100644 --- a/swift-sdk/Internal/IterableAPIInternal.swift +++ b/swift-sdk/Internal/IterableAPIInternal.swift @@ -606,6 +606,7 @@ final class IterableAPIInternal: NSObject, PushTrackerProtocol, AuthProvider { deinit { ITBInfo() + requestProcessor.stop() } } diff --git a/swift-sdk/Internal/IterableAppIntegrationInternal.swift b/swift-sdk/Internal/IterableAppIntegrationInternal.swift index 9d651ad24..107fa9267 100644 --- a/swift-sdk/Internal/IterableAppIntegrationInternal.swift +++ b/swift-sdk/Internal/IterableAppIntegrationInternal.swift @@ -129,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, @@ -162,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") } @@ -222,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 @@ -314,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 @@ -333,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/IterableTaskRunner.swift b/swift-sdk/Internal/IterableTaskRunner.swift index 31a8df927..85901b087 100644 --- a/swift-sdk/Internal/IterableTaskRunner.swift +++ b/swift-sdk/Internal/IterableTaskRunner.swift @@ -121,13 +121,17 @@ class IterableTaskRunner: NSObject { ITBInfo("Paused") return } - - DispatchQueue.global().async { + + DispatchQueue.global().async { [weak self] in ITBInfo("Scheduling timer") - let timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: false) { _ in - self.run() + guard let timeInterval = self?.timeInterval else { + return } - self.timer = timer + + 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() } @@ -224,6 +228,7 @@ class IterableTaskRunner: NSObject { deinit { ITBInfo() + stop() notificationCenter.removeObserver(self) } @@ -250,7 +255,7 @@ class IterableTaskRunner: NSObject { private let notificationCenter: NotificationCenterProtocol private let timeInterval: TimeInterval private let connectivityManager: NetworkConnectivityManager - private var timer: Timer? + private weak var timer: Timer? private var running = false private lazy var persistenceContext: IterablePersistenceContext = { diff --git a/swift-sdk/Internal/NetworkConnectivityManager.swift b/swift-sdk/Internal/NetworkConnectivityManager.swift index 8fa935c06..3443a55fc 100644 --- a/swift-sdk/Internal/NetworkConnectivityManager.swift +++ b/swift-sdk/Internal/NetworkConnectivityManager.swift @@ -31,7 +31,7 @@ class NetworkConnectivityManager: NSObject { deinit { ITBInfo() notificationCenter.removeObserver(self) - stopTimer() + stop() } var isOnline: Bool { diff --git a/swift-sdk/Internal/NetworkMonitor.swift b/swift-sdk/Internal/NetworkMonitor.swift index 38f3a9c7a..e0692deea 100644 --- a/swift-sdk/Internal/NetworkMonitor.swift +++ b/swift-sdk/Internal/NetworkMonitor.swift @@ -26,6 +26,7 @@ class NetworkMonitor: NetworkMonitorProtocol { deinit { ITBInfo() + stop() } var statusUpdatedCallback: (() -> Void)? @@ -45,9 +46,10 @@ class NetworkMonitor: NetworkMonitorProtocol { func stop() { ITBInfo() networkMonitor?.cancel() + networkMonitor = nil } - private var networkMonitor: NWPathMonitor? + private weak var networkMonitor: NWPathMonitor? private let queue = DispatchQueue(label: "NetworkMonitor") } diff --git a/swift-sdk/Internal/OfflineRequestProcessor.swift b/swift-sdk/Internal/OfflineRequestProcessor.swift index aad495b9f..296faff15 100644 --- a/swift-sdk/Internal/OfflineRequestProcessor.swift +++ b/swift-sdk/Internal/OfflineRequestProcessor.swift @@ -8,7 +8,7 @@ import Foundation @available(iOS 10.0, *) struct OfflineRequestProcessor: RequestProcessorProtocol { init(apiKey: String, - authProvider: AuthProvider, + authProvider: AuthProvider?, authManager: IterableInternalAuthManagerProtocol?, endPoint: String, deviceMetadata: DeviceMetadata, @@ -16,6 +16,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { taskRunner: IterableTaskRunner, notificationCenter: NotificationCenterProtocol ) { + ITBInfo() self.apiKey = apiKey self.authProvider = authProvider self.authManager = authManager @@ -383,6 +384,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { private class NotificationListener: NSObject { init(notificationCenter: NotificationCenterProtocol) { + ITBInfo("OfflineRequestProcessor.NotificationListener.init()") self.notificationCenter = notificationCenter super.init() self.notificationCenter.addObserver(self, @@ -394,6 +396,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol { } deinit { + ITBInfo("OfflineRequestProcessor.NotificationListener.deinit()") self.notificationCenter.removeObserver(self) } diff --git a/swift-sdk/Internal/OnlineRequestProcessor.swift b/swift-sdk/Internal/OnlineRequestProcessor.swift index 7516275ed..a45c9c15b 100644 --- a/swift-sdk/Internal/OnlineRequestProcessor.swift +++ b/swift-sdk/Internal/OnlineRequestProcessor.swift @@ -8,7 +8,7 @@ import Foundation /// `IterableAPIinternal` will delegate all network related calls to this struct. struct OnlineRequestProcessor: RequestProcessorProtocol { init(apiKey: String, - authProvider: AuthProvider, + authProvider: AuthProvider?, authManager: IterableInternalAuthManagerProtocol?, endPoint: String, networkSession: NetworkSessionProtocol, diff --git a/swift-sdk/Internal/RequestProcessor.swift b/swift-sdk/Internal/RequestProcessor.swift index 62e5bde67..3eb3aefcd 100644 --- a/swift-sdk/Internal/RequestProcessor.swift +++ b/swift-sdk/Internal/RequestProcessor.swift @@ -22,11 +22,16 @@ 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() From 0984123ec3242e22e6a04f2ed34ff803e0716390 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Wed, 30 Sep 2020 21:01:44 +0530 Subject: [PATCH 73/76] remove unneeded test --- .../TaskRunnerTests.swift | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/tests/offline-events-tests/TaskRunnerTests.swift b/tests/offline-events-tests/TaskRunnerTests.swift index cfa4d07cd..986754f86 100644 --- a/tests/offline-events-tests/TaskRunnerTests.swift +++ b/tests/offline-events-tests/TaskRunnerTests.swift @@ -213,51 +213,6 @@ class TaskRunnerTests: XCTestCase { taskRunner.stop() } - func ignore_testMultiThreading() throws { - let expectation1 = expectation(description: #function) - - struct LongRunningNetworkSession: NetworkSessionProtocol { - var statusCode = 200 - var interval: TimeInterval - - func makeRequest(_ request: URLRequest, completionHandler: @escaping CompletionHandler) { - let response = HTTPURLResponse(url: request.url!, statusCode: self.statusCode, httpVersion: "HTTP/1.1", headerFields: [:]) - DispatchQueue.global().asyncAfter(deadline: .now() + interval) { - completionHandler([:].toJsonData(), response, nil) - } - } - - func makeDataRequest(with url: URL, completionHandler: @escaping CompletionHandler) { - fatalError() - } - - func createDataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> DataTaskProtocol { - fatalError() - } - - - } - - let networkSession = LongRunningNetworkSession(interval: 1.0) - let notificationCenter = MockNotificationCenter() - let taskRunner = IterableTaskRunner(networkSession: networkSession, - persistenceContextProvider: persistenceContextProvider, - notificationCenter: notificationCenter, - timeInterval: 0.001) - taskRunner.start() - - var taskIds = [String]() - 2.times { - let taskId = try! scheduleSampleTask(notificationCenter: notificationCenter) - taskIds.append(taskId) -// Thread.sleep(forTimeInterval: 1.0) -// taskRunner.start() - } - - wait(for: [expectation1], timeout: 2.0) - taskRunner.stop() - } - private func scheduleSampleTask(notificationCenter: NotificationCenterProtocol) throws -> String { let apiKey = "zee-api-key" let eventName = "CustomEvent1" From 15ebe867c4c0ad2676df65b63b38babe3aa778d6 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 1 Oct 2020 10:48:03 +0530 Subject: [PATCH 74/76] Fix tests for memory leak --- tests/swift-sdk-swift-tests/InAppTests.swift | 97 ++++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/tests/swift-sdk-swift-tests/InAppTests.swift b/tests/swift-sdk-swift-tests/InAppTests.swift index 352699a20..bcade3afa 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,12 +509,11 @@ 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() { From 3794160dbb0df7ec9737ace33bec4c3d7b89b793 Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 1 Oct 2020 13:02:22 +0530 Subject: [PATCH 75/76] ApiClient should return `failure` instead of `fatalError` if authProvider is not present. --- swift-sdk/Internal/ApiClient.swift | 90 +++++++++++++------- tests/swift-sdk-swift-tests/InAppTests.swift | 40 ++++----- 2 files changed, 79 insertions(+), 51 deletions(-) diff --git a/swift-sdk/Internal/ApiClient.swift b/swift-sdk/Internal/ApiClient.swift index d8c8aeed7..b47694dc0 100644 --- a/swift-sdk/Internal/ApiClient.swift +++ b/swift-sdk/Internal/ApiClient.swift @@ -54,13 +54,14 @@ class ApiClient { // MARK: - Private - private func createRequestCreator() -> 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, @@ -86,40 +87,52 @@ class ApiClient { extension ApiClient: ApiClientProtocol { func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Future { - send(iterableRequestResult: createRequestCreator().createRegisterTokenRequest(registerTokenInfo: registerTokenInfo, - notificationsEnabled: notificationsEnabled)) + 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, @@ -128,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) } } @@ -170,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/tests/swift-sdk-swift-tests/InAppTests.swift b/tests/swift-sdk-swift-tests/InAppTests.swift index bcade3afa..b59cd4dd7 100644 --- a/tests/swift-sdk-swift-tests/InAppTests.swift +++ b/tests/swift-sdk-swift-tests/InAppTests.swift @@ -515,26 +515,23 @@ class InAppTests: XCTestCase { 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 @@ -545,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, @@ -554,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" @@ -1472,7 +1468,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() From 44f99b4a5350740d5f3f2be70207e20b3da66b0a Mon Sep 17 00:00:00 2001 From: Tapash Majumder Date: Thu, 1 Oct 2020 19:57:12 +0530 Subject: [PATCH 76/76] Fix more memory leaks in unit tests --- tests/swift-sdk-swift-tests/InAppTests.swift | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/swift-sdk-swift-tests/InAppTests.swift b/tests/swift-sdk-swift-tests/InAppTests.swift index b59cd4dd7..0eec58be5 100644 --- a/tests/swift-sdk-swift-tests/InAppTests.swift +++ b/tests/swift-sdk-swift-tests/InAppTests.swift @@ -621,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() } @@ -1043,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() } @@ -1234,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) @@ -1359,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