diff --git a/CHANGELOG.md b/CHANGELOG.md index 3899a4c..dd54310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # CHANGELOG All notable changes to this project will be documented in this file. +## 3.1.0 +* Errors - Create recorder for sharing important errors +* Errors - Refact error to be more precise on the failure +[Anthony GUIGUEN](https://https://github.com/anthonyGuiguen) +[#34](https://github.com/Insurlytech/zetapush-swift/pull/34) + ## 3.0.0 * CometD - Remove CometDClient from ZetaPush [Anthony GUIGUEN](https://https://github.com/anthonyGuiguen) diff --git a/Package.resolved b/Package.resolved index 51bd508..ec56e44 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Insurlytech/CometDClient-iOS.git", "state": { "branch": null, - "revision": "7dc8edfe6af41db6c6e64bf8541b64d2c93a559a", - "version": "1.0.0" + "revision": "44ce6ecd3594a670ea59d375ff69af4c090d0edb", + "version": "1.1.0" } }, { @@ -52,7 +52,7 @@ "state": { "branch": null, "revision": "2b6054efa051565954e1d2b9da831680026cd768", - "version": "4.3.0" + "version": "5.0.0" } }, { diff --git a/Package.swift b/Package.swift index 3c3cf7d..6cd2d19 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(name: "CometDClient", url: "https://github.com/Insurlytech/CometDClient-iOS.git", .upToNextMajor(from: "1.0.0")), + .package(name: "CometDClient", url: "https://github.com/Insurlytech/CometDClient-iOS.git", .upToNextMajor(from: "1.1.0")), // .package(name: "CometDClient", path: "../CometDClient-iOS"), //AGU .package(url: "https://github.com/hkellaway/Gloss.git", .upToNextMajor(from: "3.1.0")), .package(url: "https://github.com/mxcl/PromiseKit", .upToNextMajor(from: "6.13.1")), diff --git a/Sources/ZetaPushNetwork/Client/ClientHelper.swift b/Sources/ZetaPushNetwork/Client/ClientHelper.swift index 70c005f..61c3665 100644 --- a/Sources/ZetaPushNetwork/Client/ClientHelper.swift +++ b/Sources/ZetaPushNetwork/Client/ClientHelper.swift @@ -18,9 +18,9 @@ import UIKit // MARK: - ClientHelper open class ClientHelper: NSObject { // MARK: Properties - var sandboxId = "" + let serverConfiguration: ServerConfiguration + private let remote: ServerRemoteDataSource var server = "" - var apiUrl = "" var connected = false var userId = "" var resource = "" @@ -44,28 +44,30 @@ open class ClientHelper: NSObject { let log = XCGLogger(identifier: "zetaPushLogger", includeDefaultDestinations: true) let tags = XCGLogger.Constants.userInfoKeyTags - public var timeout: TimeInterval = ZetaPushConstants.timeout + private weak var recorder: ZetapushNetworkRecorder? // MARK: Lifecycle - public init(apiUrl: String, sandboxId: String, authentication: AbstractHandshake, resource: String = "", logLevel: XCGLogger.Level = .severe) { - self.sandboxId = sandboxId + public init(serverConfiguration: ServerConfiguration, authentication: AbstractHandshake, resource: String = "", logLevel: XCGLogger.Level = .severe, recorder: ZetapushNetworkRecorder?) { + self.serverConfiguration = serverConfiguration + self.recorder = recorder + self.remote = ServerRemoteDataSource(configuration: serverConfiguration, recorder: recorder) self.authentication = authentication self.resource = resource - self.apiUrl = apiUrl self.cometdClient = CometdClient() super.init() self.logLevel = logLevel log.setup(level: logLevel) + self.cometdClient.log.outputLevel = self.logLevel // Handle resource let defaults = UserDefaults.standard if resource.isEmpty { - if let storedResource = defaults.string(forKey: zetaPushDefaultKeys.resource) { + if let storedResource = defaults.string(forKey: ZetaPushDefaultKeys.resource) { self.resource = storedResource } else { self.resource = ZetaPushUtils.generateResourceName() - defaults.set(self.resource, forKey: zetaPushDefaultKeys.resource) + defaults.set(self.resource, forKey: ZetaPushDefaultKeys.resource) } } @@ -93,67 +95,37 @@ open class ClientHelper: NSObject { // Connect to server open func connect() { - log.debug("Client Connection: check the validation of server url : \(server)") + log.zp.debug("ZetaPushNetwork try to connect") guard server.isEmpty else { - log.zp.debug("Client Connection: ZetaPush configured Server") - log.zp.debug(self.server) + log.zp.debug("ZetaPushNetwork already has a server url") configureCometdClient() return } - let stringUrl = self.apiUrl + "/" + sandboxId - guard let url = URL(string: stringUrl), UIApplication.shared.canOpenURL(url) else { - self.log.verbose("ZP server -> can't open url : " + stringUrl) - return - } - - // Check the http://api.zpush.io with sandboxId - self.log.verbose("ZP server -> target url : " + url.description) - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = timeout - configuration.timeoutIntervalForResource = timeout * 3 - let task = URLSession(configuration: configuration).dataTask(with: url) { [weak self] data, response, error in + log.zp.debug("ZetaPushNetwork try to fetch servers URLs") + remote.fetchServersURLs { [weak self] result in guard let self = self else { return } - guard let data = data else { - self.log.zp.error("Client Connection: No server for the sandbox") - return - } - guard error == nil else { - self.log.error (error!) - return - } - - self.log.verbose("ZP server -> server response data : " + data.description) - let jsonAnyTest = try? JSONSerialization.jsonObject(with: data, options: []) - let jsonTest = jsonAnyTest as? [String: Any] ?? [:] - self.log.verbose("ZP server -> server response : " + jsonTest.description) - - guard let jsonAny = try? JSONSerialization.jsonObject(with: data, options: []), - let json = jsonAny as? [String : Any], - let servers = json["servers"] as? [Any] else { - self.log.zp.error("Client Connection: Failed to parse data from server") - return - } - - guard let randomServer = servers.randomElement() as? String else { - self.log.zp.error("Client Connection: No server in servers object") - return + switch result { + case .success(let servers): + self.log.zp.debug("ZetaPushNetwork succeded to fetch servers URLs : \(servers)") + guard let randomServer = servers.randomElement() else { return } + self.log.zp.debug("ZetaPushNetwork select a random server : \(randomServer)") + self.server = randomServer + "/strd" + + self.configureCometdClient() + case .failure(let error): + self.log.zp.error("ZetaPushNetwork failed to fetch servers URLs : \(error.localizedDescription)") + self.delegate?.onConnectionFailed(self, error: ClientHelperError.connectionFailed) } - self.server = randomServer + "/strd" - self.log.debug("Client Connection: ZetaPush selected Server") - self.log.debug("Client Connection: server returned server url : \(self.server)") - - self.cometdClient.log.outputLevel = self.logLevel - self.configureCometdClient() } - task.resume() } private func configureCometdClient() { - cometdClient.configure(url: server) + log.zp.debug("ZetaPushNetwork configure CometdClient") + cometdClient.configure(url: server, recorder: recorder) let handshakeFields = authentication.getHandshakeFields(self) - self.log.debug("authentification = \(authentication)") + log.zp.debug("authentification = \(authentication)") cometdClient.handshake(fields: handshakeFields) } @@ -204,7 +176,7 @@ open class ClientHelper: NSObject { } open func composeServiceChannel(_ verb: String, deploymentId: String) -> String { - return "/service/" + sandboxId + "/" + deploymentId + "/" + verb + return "/service/" + serverConfiguration.sandboxId + "/" + deploymentId + "/" + verb } open func getLogLevel() -> XCGLogger.Level { @@ -237,7 +209,7 @@ open class ClientHelper: NSObject { } open func getSandboxId() -> String { - return sandboxId + return serverConfiguration.sandboxId } open func getServer() -> String { @@ -324,16 +296,28 @@ extension ClientHelper: CometdClientDelegate { delegate?.onSuccessfulHandshake(self) } - - public func handshakeDidFailed(from client: CometdClientContract) { + + public func handshakeDidFailed(error: Error, from client: CometdClientContract) { log.zp.error("ClientHelper Handshake Failed") - delegate?.onFailedHandshake(self) + var handshakeError: ClientHelperError = .handshakeFailed(reason: nil) + if let error = error as? CometDClientError { + switch error { + case .handshake(let reason): + handshakeError = .handshakeFailed(reason: reason) + default: break + } + } + delegate?.onFailedHandshake(self, error: handshakeError) } public func didDisconnected(error: Error?, from client: CometdClientContract) { log.zp.debug("ClientHelper Disconnected from Cometd server") connected = false - delegate?.onConnectionClosed(self) + if error != nil { + delegate?.onConnectionClosed(self, error: ClientHelperError.connectionClosed) + } else { + delegate?.onConnectionClosed(self, error: nil) + } } public func didAdvisedToReconnect(from client: CometdClientContract) { @@ -341,14 +325,14 @@ extension ClientHelper: CometdClientDelegate { delegate?.onConnectionClosedAdviceReconnect(self) } - public func didFailConnection(error: Error?, from client: CometdClientContract) { - log.zp.error("ClientHelper Failed to connect to Cometd server!") + public func didLostConnection(error: Error, from client: CometdClientContract) { + log.zp.error("ClientHelper lost connection") if wasConnected { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(automaticReconnectionDelay)) { [weak self] in self?.connect() } } - delegate?.onConnectionBroken(self) + delegate?.onConnectionBroken(self, error: ClientHelperError.connectionBroken) } public func didSubscribeToChannel(channel: String, from client: CometdClientContract) { @@ -361,9 +345,9 @@ extension ClientHelper: CometdClientDelegate { delegate?.onDidUnsubscribeFromChannel(self, channel: channel) } - public func subscriptionFailedWithError(error: SubscriptionError, from client: CometdClientContract) { + public func subscriptionFailedWithError(error: Error, from client: CometdClientContract) { log.zp.error("ClientHelper Subscription failed") - delegate?.onSubscriptionFailedWithError(self, error: error) + delegate?.onSubscriptionFailedWithError(self, error: ClientHelperError.subscription) } public func didWriteError(error: Error, from client: CometdClientContract) { diff --git a/Sources/ZetaPushNetwork/Client/ClientHelperDelegate.swift b/Sources/ZetaPushNetwork/Client/ClientHelperDelegate.swift index 96963ce..1954930 100644 --- a/Sources/ZetaPushNetwork/Client/ClientHelperDelegate.swift +++ b/Sources/ZetaPushNetwork/Client/ClientHelperDelegate.swift @@ -11,25 +11,27 @@ import Foundation // MARK: CometdClientDelegate public protocol ClientHelperDelegate: class { + func onConnectionFailed(_ client: ClientHelper, error: Error) func onConnectionEstablished(_ client: ClientHelper) - func onConnectionBroken(_ client: ClientHelper) - func onConnectionClosed(_ client: ClientHelper) + func onConnectionBroken(_ client: ClientHelper, error: Error) + func onConnectionClosed(_ client: ClientHelper, error: Error?) func onConnectionClosedAdviceReconnect(_ client: ClientHelper) func onSuccessfulHandshake(_ client: ClientHelper) - func onFailedHandshake(_ client: ClientHelper) + func onFailedHandshake(_ client: ClientHelper, error: Error) func onDidSubscribeToChannel(_ client: ClientHelper, channel: String) func onDidUnsubscribeFromChannel(_ client: ClientHelper, channel: String) - func onSubscriptionFailedWithError(_ client: ClientHelper, error: SubscriptionError) + func onSubscriptionFailedWithError(_ client: ClientHelper, error: Error) } public extension ClientHelperDelegate { + func onConnectionFailed(_ client: ClientHelper, error: Error) { } func onConnectionEstablished(_ client: ClientHelper) { } - func onConnectionBroken(_ client: ClientHelper) { } - func onConnectionClosed(_ client: ClientHelper) { } + func onConnectionBroken(_ client: ClientHelper, error: Error) { } + func onConnectionClosed(_ client: ClientHelper, error: Error?) { } func onConnectionClosedAdviceReconnect(_ client: ClientHelper) { } func onSuccessfulHandshake(_ client: ClientHelper) { } - func onFailedHandshake(_ client: ClientHelper) { } + func onFailedHandshake(_ client: ClientHelper, error: Error) { } func onDidSubscribeToChannel(_ client: ClientHelper, channel: String) { } func onDidUnsubscribeFromChannel(_ client: ClientHelper, channel: String) { } - func onSubscriptionFailedWithError(_ client: ClientHelper, error: SubscriptionError) { } + func onSubscriptionFailedWithError(_ client: ClientHelper, error: Error) { } } diff --git a/Sources/ZetaPushNetwork/Client/ClientHelperError.swift b/Sources/ZetaPushNetwork/Client/ClientHelperError.swift new file mode 100644 index 0000000..0e032be --- /dev/null +++ b/Sources/ZetaPushNetwork/Client/ClientHelperError.swift @@ -0,0 +1,15 @@ +// +// ClientHelperError.swift +// +// +// Created by Anthony Guiguen on 22/07/2020. +// + +import Foundation +import CometDClient + +public typealias HandshakeError = CometDClient.HandshakeError + +public enum ClientHelperError: Error { + case connectionFailed, connectionBroken, connectionClosed, handshakeFailed(reason: HandshakeError?), subscription +} diff --git a/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSource.swift b/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSource.swift new file mode 100644 index 0000000..6d14182 --- /dev/null +++ b/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSource.swift @@ -0,0 +1,80 @@ +// +// ServerRemoteDataSource.swift +// +// +// Created by Anthony Guiguen on 22/07/2020. +// + +import Foundation +import UIKit + +public struct ServerConfiguration { + var serverUrl: String + let sandboxId: String + var timeout: TimeInterval +} + +class ServerRemoteDataSource { + + // MARK: Properties + private let url: URL? + private let session: URLSession! + private var task: URLSessionDataTask? + private weak var recorder: ZetapushNetworkRecorder? + + // MARK: Lifecycle + init(configuration: ServerConfiguration, recorder: ZetapushNetworkRecorder?) { + self.url = URL(string: configuration.serverUrl)?.appendingPathComponent(configuration.sandboxId) + self.recorder = recorder + + let timeout = configuration.timeout + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = timeout + configuration.timeoutIntervalForResource = timeout * 3 + + self.session = URLSession(configuration: configuration) + } + + // MARK: Methods + func fetchServersURLs(callback: @escaping (Result<[String], Error>) -> Void) { + guard let url = url, UIApplication.shared.canOpenURL(url) else { + let error: ServerRemoteDataSourceError = .canNotOpenURL(url: self.url?.absoluteString ?? "") + recorder?.record(error: error.toNSError()) + callback(.failure(error)) + return + } + task?.cancel() + + task = session.dataTask(with: url) { [weak self] data, response, error in + defer { self?.task = nil } + + if let error = error { + self?.recorder?.record(error: error as NSError) + callback(.failure(error)) + } else if let response = response as? HTTPURLResponse, response.statusCode != 200 { + let error: ServerRemoteDataSourceError = .response(response: response) + self?.recorder?.record(error: error.toNSError()) + callback(.failure(error)) + } else if let data = data { + do { + let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + if let servers = json?["servers"] as? [String] { + callback(.success(servers)) + } else { + let error: ServerRemoteDataSourceError = .serversNotFound + self?.recorder?.record(error: error.toNSError()) + callback(.failure(error)) + } + } catch let error { + self?.recorder?.record(error: error as NSError) + callback(.failure(error)) + } + } else { + let error: ServerRemoteDataSourceError = .unknown + self?.recorder?.record(error: error.toNSError()) + callback(.failure(error)) + } + } + task?.resume() + } +} diff --git a/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSourceError.swift b/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSourceError.swift new file mode 100644 index 0000000..48cd015 --- /dev/null +++ b/Sources/ZetaPushNetwork/Client/Server/ServerRemoteDataSourceError.swift @@ -0,0 +1,46 @@ +// +// File.swift +// +// +// Created by Anthony Guiguen on 22/07/2020. +// + +import Foundation + +enum ErrorConstant { + static let domain = "zetaPushNetwork" + static let code = "code" +} + +public enum ServerRemoteDataSourceError: Error { + case canNotOpenURL(url: String), response(response: HTTPURLResponse), serversNotFound, unknown + + func toNSError() -> NSError { + switch self { + case .canNotOpenURL(let url): + return NSError(domain: ErrorConstant.domain, code: 403, userInfo: [ + NSLocalizedDescriptionKey: NSLocalizedString("Can not open URL", comment: ""), + NSLocalizedFailureReasonErrorKey: NSLocalizedString("Can not open this url : \(url).", comment: ""), + ErrorConstant.code: "ERROR_CLIENT_HELPER_CAN_NOT_OPEN_URL" + ]) + case .response(let response): + return NSError(domain: ErrorConstant.domain, code: response.statusCode, userInfo: [ + NSLocalizedDescriptionKey: NSLocalizedString("The request failed.", comment: ""), + NSLocalizedFailureReasonErrorKey: NSLocalizedString("The fetchServersURLs(callback:) response returned a \(response.statusCode).", comment: ""), + ErrorConstant.code: "ERROR_CLIENT_HELPER_RESPONSE" + ]) + case .serversNotFound: + return NSError(domain: ErrorConstant.domain, code: 404, userInfo: [ + NSLocalizedDescriptionKey: NSLocalizedString("Empty list of server.", comment: ""), + NSLocalizedFailureReasonErrorKey: NSLocalizedString("The fetchServersURLs(callback:) response has an empty list of server.", comment: ""), + ErrorConstant.code: "ERROR_CLIENT_HELPER_SERVERS_NOT_FOUND" + ]) + case .unknown: + return NSError(domain: ErrorConstant.domain, code: 456, userInfo: [ + NSLocalizedDescriptionKey: NSLocalizedString("Ooops an error occured", comment: ""), + NSLocalizedFailureReasonErrorKey: NSLocalizedString("Unknown error", comment: ""), + ErrorConstant.code: "ERROR_CLIENT_HELPER_UNKNOWN" + ]) + } + } +} diff --git a/Sources/ZetaPushNetwork/Client/default/ZetaPushClient.swift b/Sources/ZetaPushNetwork/Client/default/ZetaPushClient.swift index 28dc9a2..a1fde89 100644 --- a/Sources/ZetaPushNetwork/Client/default/ZetaPushClient.swift +++ b/Sources/ZetaPushNetwork/Client/default/ZetaPushClient.swift @@ -9,14 +9,15 @@ import Foundation import Gloss -enum zetaPushDefaultConfig { +enum ZetaPushDefaultConfig { static let apiUrl = "https://api.zpush.io" static let weakDeploymentId = "weak_0" static let simpleDeploymentId = "simple_0" static let macroDeployementId = "macro_0" static let resourceLength = 8 + static let timeout: TimeInterval = 45 } -enum zetaPushDefaultKeys{ +enum ZetaPushDefaultKeys { static let sandboxId = "zetapush.sandboxId" static let token = "zetapush.token" static let publicToken = "zetapush.publicToken" @@ -52,6 +53,6 @@ open class ZPMessage { public enum ZetaPushUtils { static func generateResourceName() -> String { let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - return String((0.. String { diff --git a/Sources/ZetaPushNetwork/Model/ZetapushNetworkRecorder.swift b/Sources/ZetaPushNetwork/Model/ZetapushNetworkRecorder.swift new file mode 100644 index 0000000..911f711 --- /dev/null +++ b/Sources/ZetaPushNetwork/Model/ZetapushNetworkRecorder.swift @@ -0,0 +1,12 @@ +// +// ZetapushNetworkRecorder.swift +// +// +// Created by Anthony Guiguen on 24/07/2020. +// + +import CometDClient +import Foundation + +/// Implement this protocol if you want to catch precise error for analytics or debug +public protocol ZetapushNetworkRecorder: CometDClientRecorder { } diff --git a/Sources/ZetaPushNetwork/Service/ZetaPushServiceDelegate.swift b/Sources/ZetaPushNetwork/Service/ZetaPushServiceDelegate.swift deleted file mode 100644 index 8f2d586..0000000 --- a/Sources/ZetaPushNetwork/Service/ZetaPushServiceDelegate.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ZetaPushServiceDelegate.swift -// ZetaPush -// -// Created by Leocare on 19/04/2019. -// Copyright © 2019 Leocare. All rights reserved. -// - -import CometDClient -import Foundation - -// MARK: - ZetaPushServiceDelegate -public protocol ZetaPushServiceDelegate: NSObjectProtocol { - func onDidSubscribeToChannel(_ client: ClientHelper, channel: String) - func onDidUnsubscribeFromChannel(_ client: ClientHelper, channel: String) - func onSubscriptionFailedWithError(_ client: ClientHelper, error: SubscriptionError) -} - -public extension ZetaPushServiceDelegate { - func onDidSubscribeToChannel(_ client: ClientHelper, channel: String) { } - func onDidUnsubscribeFromChannel(_ client: ClientHelper, channel: String) { } - func onSubscriptionFailedWithError(_ client: ClientHelper, error: SubscriptionError) { } -} diff --git a/Sources/ZetaPushNetwork/ZetaPushConstants.swift b/Sources/ZetaPushNetwork/ZetaPushConstants.swift deleted file mode 100644 index 4d54598..0000000 --- a/Sources/ZetaPushNetwork/ZetaPushConstants.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ZetaPushConstants.swift -// ZetaPush -// -// Created by Anthony Guiguen on 04/07/2019. -// Copyright © 2019 ZetaPush. All rights reserved. -// - -import Foundation - -public enum ZetaPushConstants { - static let timeout: TimeInterval = 45 -} diff --git a/ZetaPushNetwork.podspec b/ZetaPushNetwork.podspec index ecf18a3..fe4dd50 100644 --- a/ZetaPushNetwork.podspec +++ b/ZetaPushNetwork.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |spec| spec.name = "ZetaPushNetwork" - spec.version = "3.0.0" + spec.version = "3.1.0" spec.summary = "Swift client for ZetaPush" spec.description = <<-DESC Swift client for ZetaPush @@ -28,5 +28,5 @@ Pod::Spec.new do |spec| spec.dependency "PromiseKit", "~> 6.13" spec.dependency "Gloss", "~> 3.0" - spec.dependency "CometDClient", "~> 1.0.0" + spec.dependency "CometDClient", "~> 1.1.0" end