Skip to content

Commit

Permalink
Fixed issues and other improvements (#22)
Browse files Browse the repository at this point in the history
* Fixed #20: Added custom timeout for each request

* Fixed #19: Added progress to post

* Fixed #18: Added character limit for debug log

* Fixed #21: Serialized signed requests

* Resolved #17: Response delegate

* Resolved #14: Added more known API errors

* Fixed #16: Improved WPNError

* Reintroduced domain/code/userInfo as public

* Improved rest api error

* SwiftLint fixes

* Fixed post progress api availability

* Small changes
  • Loading branch information
kober32 authored Dec 23, 2021
1 parent 30a7dc4 commit 83500cc
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 171 deletions.
48 changes: 44 additions & 4 deletions Sources/WultraPowerauthNetworking/WPNBaseNetworkingObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,42 @@ public class WPNResponseArray<T: Decodable>: WPNResponseBase {
}
}

/// Known values of rest api errors
/// Known values of REST API errors
public enum WPNKnownRestApiError: String, Decodable {

// COMMON ERRORS

/// When unexpected error happened.
case genericError = "ERROR_GENERIC"

/// General authentication failure (wrong password, wrong activation state, etc...)
case authenticationFailure = "POWERAUTH_AUTH_FAIL"

/// Failed to register push notifications
case pushRegistrationFailed = "PUSH_REGISTRATION_FAILED"

/// Invalid request sent - missing request object in request
case invalidRequest = "INVALID_REQUEST"

/// Activation is not valid (it is different from configured activation)
case invalidActivation = "INVALID_ACTIVATION"

/// Error during activfation
case activationError = "ERR_ACTIVATION"

/// Error in case that PowerAuth authentication fails
case authenticationError = "ERR_AUTHENTICATION"

/// Error during secure vault unlocking
case secureVaultError = "ERR_SECURE_VAULT"

/// Returned in case encryption or decryption fails
case encryptionError = "ERR_ENCRYPTION"

// PUSH ERRORS

/// Failed to register push notifications
case pushRegistrationFailed = "PUSH_REGISTRATION_FAILED"

// OPERATIONS ERRORS

/// Operation is already finished
case operationAlreadyFinished = "OPERATION_ALREADY_FINISHED"

Expand All @@ -152,6 +173,25 @@ public enum WPNKnownRestApiError: String, Decodable {

/// Operation is expired
case operationExpired = "OPERATION_EXPIRED"

// ACTIVATION SPAWN ERRORS

/// Unable to fetch activation code.
case activationCodeFailed = "ACTIVATION_CODE_FAILED"

// IDENTITY ONBOARDING ERRORS

/// Onboarding process failed or failed to start
case onboardingFailed = "ONBOARDING_FAILED"

/// Failed to resend onboarding OTP (probably requested too soon)
case onboardingOtpFailed = "ONBOARDING_OTP_FAILED"

/// Document is invalid
case invalidDocument = "INVALID_DOCUMENT"

/// Identity verification failed
case identityVerificationFailed = "IDENTITY_VERIFICATION_FAILED"
}

/// Error passed in a response, when the error is returned from an endpoint.
Expand Down
10 changes: 10 additions & 0 deletions Sources/WultraPowerauthNetworking/WPNConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,18 @@ public struct WPNConfig {
public let sslValidation: WPNSSLValidationStrategy

/// The timeout interval to use when waiting for backend data.
///
/// Value can be overridden for each `post` call in `WPNNetworkingService`.
/// Default value is 20.
public let timeoutIntervalForRequest: TimeInterval

/// Create instance of the config
/// - Parameters:
/// - baseUrl: Base URL for service requests.
/// - sslValidation: SSL validation strategy for the request.
/// - timeoutIntervalForRequest: The timeout interval to use when waiting for backend data.
/// Value can be overridden for each `post` call in `WPNNetworkingService`.
/// Default value is 20.
public init(baseUrl: URL, sslValidation: WPNSSLValidationStrategy, timeoutIntervalForRequest: TimeInterval = 20) {
self.baseUrl = baseUrl
self.sslValidation = sslValidation
Expand Down
217 changes: 102 additions & 115 deletions Sources/WultraPowerauthNetworking/WPNError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,21 @@ public class WPNError: Error {

public init(reason: WPNErrorReason, error: Error? = nil) {
#if DEBUG
WPNError.validateNestedError(error)
if let error = error as? WPNError {
D.error("You should not embed WPNError into another WPNError object. Please use .wrap() function if you're not sure what type of error is passed to initializer. Error: \(error.localizedDescription)")
}
#endif
self.reason = reason
self.nestedError = error
}

/// Private initializer
fileprivate init(reason: WPNErrorReason,
nestedError: Error?,
httpStatusCode: Int,
httpUrlResponse: HTTPURLResponse?,
restApiError: WPNRestApiError?) {
self.nestedError = nestedError
self.reason = reason
self._httpStatusCode = httpStatusCode
fileprivate convenience init(reason: WPNErrorReason, nestedError: Error?, httpUrlResponse: HTTPURLResponse?, restApiError: WPNRestApiError?) {
self.init(reason: reason, error: nestedError)
self.httpUrlResponse = httpUrlResponse
self.restApiError = restApiError
}

#if DEBUG
private static func validateNestedError(_ error: Error?) {
if let error = error as? WPNError {
D.error("You should not embed WPNError into another WPNError object. Please use .wrap() function if you're not sure what type of error is passed to initializer. Error: \(error.localizedDescription)")
}
}
#endif

// MARK: - Properties

/// Reason why the error was created
Expand All @@ -57,62 +45,105 @@ public class WPNError: Error {
/// Nested error.
public let nestedError: Error?

/// HTTP status code.
/// A full response received from the server.
///
/// If value is not set, then it is automatically gathered from
/// the nested error or from `URLResponse`. Also the nested error must be produced
/// in PowerAuth2 library and contain embedded `PowerAuthRestApiErrorResponse` object.
/// If you set a valid object to this property, then the `httpStatusCode` starts
/// returning status code from the response. You can set this value in cases that,
/// it's important to investigate a whole response, after the authentication fails.
///
/// Due to internal getter optimization, the nested objects evaluation is performed only once.
/// So if you get the value before URL response is set, then the returned value will be incorrect.
/// You can still later override the calculated value by setting a new one.
public var httpStatusCode: Int {
/// Normally, setting `httpStatusCode` is enough for proper handling authentication errors.
internal(set) public var httpUrlResponse: HTTPURLResponse?

private var _restApiError: WPNRestApiError? // backing field

/// An optional error describing details about REST API failure.
internal(set) public var restApiError: WPNRestApiError? {
get {
if _httpStatusCode >= 0 {
return _httpStatusCode
} else if let httpUrlResponse = httpUrlResponse {
_httpStatusCode = Int(httpUrlResponse.statusCode)
} else if let responseObject = self.powerAuthErrorResponse {
_httpStatusCode = Int(responseObject.httpStatusCode)
} else {
_httpStatusCode = 0
if let rae = _restApiError {
return rae
}
if let pae = powerAuthRestApiError?.responseObject {
return WPNRestApiError(code: pae.code, message: pae.message)
}
return _httpStatusCode
return nil
}
set {
_httpStatusCode = newValue
_restApiError = newValue
}
}

/// Private value for httpStatusCode property.
private var _httpStatusCode: Int = -1
// MARK: - Computed properties

/// A full response received from the server.
/// HTTP status code.
///
/// If you set a valid object to this property, then the `httpStatusCode` starts
/// returning status code from the response. You can set this value in cases that,
/// it's important to investigate a whole response, after the authentication fails.
/// nil if not available (not an HTTP error).
public var httpStatusCode: Int? {
if let httpUrlResponse = httpUrlResponse {
return Int(httpUrlResponse.statusCode)
} else if let responseObject = powerAuthRestApiError {
return Int(responseObject.httpStatusCode)
} else {
return nil
}
}

/// Returns `PowerAuthRestApiErrorResponse` if such object is embedded in nested error. This is typically useful
/// for getting error HTTP response created in the PowerAuth2 library.
public var powerAuthRestApiError: PowerAuthRestApiErrorResponse? {
return userInfo[PowerAuthErrorDomain] as? PowerAuthRestApiErrorResponse
}

/// Returns PowerAuth error code when the error was caused by the PowerAuth2 library.
///
/// Normally, setting `httpStatusCode` is enough for proper handling authentication errors.
public var httpUrlResponse: HTTPURLResponse?
/// For possible values, visit [PowerAuth Documentation](https://developers.wultra.com/components/powerauth-mobile-sdk/develop/documentation/PowerAuth-SDK-for-iOS#error-handling)
public var powerAuthErrorCode: PowerAuthErrorCode? {
return (nestedError as NSError?)?.powerAuthErrorCode
}

/// An optional error describing details about REST API failure.
public var restApiError: WPNRestApiError?
}

// MARK: - Wrapping Error into WPNError

public extension WPNError {
/// Returns error message when the underlying error was caused by the PowerAuth2 library.
public var powerAuthErrorMessage: String? {
guard domain == PowerAuthErrorDomain else {
return nil
}
return userInfo[NSLocalizedDescriptionKey] as? String
}

/// Returns true if the error is caused by the missing network connection.
/// The device is typically not connected to the internet.
public var networkIsNotReachable: Bool {
if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String {
let ec = CFNetworkErrors(rawValue: Int32(code))
return ec == .cfurlErrorNotConnectedToInternet ||
ec == .cfurlErrorInternationalRoamingOff ||
ec == .cfurlErrorDataNotAllowed
}
return false
}

/// Returns true if the error is related to the connection security - like untrusted TLS
/// certificate, or similar TLS related problems.
public var networkConnectionIsNotTrusted: Bool {
if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String {
let code = Int32(code)
if code == CFNetworkErrors.cfurlErrorServerCertificateHasBadDate.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateUntrusted.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateHasUnknownRoot.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateNotYetValid.rawValue ||
code == CFNetworkErrors.cfurlErrorSecureConnectionFailed.rawValue {
return true
}
}
return false
}

/// Returns WPNError object with nested error and additional nested description.
/// If the provided error object is already WPNError, then returns copy of the object,
/// with modiffied nested description.
static func wrap(_ reason: WPNErrorReason, _ error: Error? = nil) -> WPNError {
/// with modified nested description.
public static func wrap(_ reason: WPNErrorReason, _ error: Error? = nil) -> WPNError {
if let error = error as? WPNError {
return WPNError(
reason: reason,
nestedError: error.nestedError,
httpStatusCode: error._httpStatusCode,
httpUrlResponse: error.httpUrlResponse,
restApiError: error.restApiError)
}
Expand All @@ -134,91 +165,37 @@ public struct WPNErrorReason: RawRepresentable, Equatable, Hashable {
}
}

// MARK: - Computed properties

public extension WPNError {

/// A fallback domain identifier which is returned in situations, when the nested error
/// is not set, or if it's not kind of NSError object.
static let domain = "WPNError"

/// If nestedError is valid, then returns its code
var code: Int {
guard let e = nestedError as NSError? else {
return 0
}
return e.code
}

/// If nestedError is valid, then returns its domain.
/// Otherwise returns `WPNError.domain`
var domain: String {
guard let e = nestedError as NSError? else {
return WPNError.domain
return "WPNError"
}
return e.domain
}

/// If nestedError is valid, then returns its user info.
var userInfo: [String: Any] {
guard let e = nestedError as NSError? else {
return [:]
}
return e.userInfo
}

/// Returns true if nested error has information about missing network connection.
/// The device is typically not connected to the internet.
var networkIsNotReachable: Bool {
if self.domain == NSURLErrorDomain || self.domain == kCFErrorDomainCFNetwork as String {
let ec = CFNetworkErrors(rawValue: Int32(self.code))
return ec == .cfurlErrorNotConnectedToInternet ||
ec == .cfurlErrorInternationalRoamingOff ||
ec == .cfurlErrorDataNotAllowed
}
return false
}

/// Returns true if nested error has information about connection security, like untrusted TLS
/// certificate, or similar TLS related problems.
var networkConnectionIsNotTrusted: Bool {
let domain = self.domain
if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String {
let code = Int32(self.code)
if code == CFNetworkErrors.cfurlErrorServerCertificateHasBadDate.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateUntrusted.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateHasUnknownRoot.rawValue ||
code == CFNetworkErrors.cfurlErrorServerCertificateNotYetValid.rawValue ||
code == CFNetworkErrors.cfurlErrorSecureConnectionFailed.rawValue {
return true
}
}
return false
}

/// Returns `PowerAuthRestApiErrorResponse` if such object is embedded in nested error. This is typically useful
/// for getting response created in the PowerAuth2 library.
var powerAuthErrorResponse: PowerAuthRestApiErrorResponse? {
if let responseObject = self.userInfo[PowerAuthErrorDomain] as? PowerAuthRestApiErrorResponse {
return responseObject
}
return nil
}

var powerAuthRestApiErrorCode: String? {
if let response = restApiError {
return response.code
}
if let code = powerAuthErrorResponse?.responseObject?.code {
return code
}
return nil
}
}

extension WPNError: CustomStringConvertible {
public var description: String {

if let powerAuthErrorMessage = powerAuthErrorMessage {
return powerAuthErrorMessage
}

if let nsne = nestedError as NSError? {
return nsne.description
}
Expand All @@ -227,12 +204,12 @@ extension WPNError: CustomStringConvertible {

result += "\nError domain: \(domain), code: \(code)"

if httpStatusCode != -1 {
if let httpStatusCode = httpStatusCode {
result += "\nHTTP Status Code: \(httpStatusCode)"
}

if let raec = powerAuthRestApiErrorCode {
result += "\nPA REST API Code: \(raec)"
if let powerAuthRestApiError = powerAuthRestApiError {
result += "\nPA REST API Code: \(powerAuthRestApiError)"
}

return result
Expand All @@ -244,3 +221,13 @@ extension D {
D.error(error().description)
}
}

/// Simple error class to add developer comment when throwing an WPNError
internal class WPNSimpleError: Error {

let localizedDescription: String

init(message: String) {
localizedDescription = message
}
}
Loading

0 comments on commit 83500cc

Please sign in to comment.