Skip to content

Commit

Permalink
Merge branch 'feature/VPN-1691-throttling-header-client-handling' int…
Browse files Browse the repository at this point in the history
…o 'develop'

Handles user interaction on authentication screens for throttling headers

See merge request pia-mobile/ios/client-library-apple!297
  • Loading branch information
Waleed Mahmood committed Feb 7, 2022
2 parents 5e9af4d + 6d34ee3 commit 1d95c91
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 24 deletions.
2 changes: 1 addition & 1 deletion PIALibrary/Resources/UI/iOS/en.lproj/Welcome.strings
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"login.error.title" = "Log in";
"login.error.validation" = "You must enter a username and password.";
"login.error.unauthorized" = "Your username or password is incorrect.";
"login.error.throttled" = "Too many failed login attempts with this username. Please try again later.";
"login.error.throttled" = "Too many failed login attempts with this username. Please try again after %@ second(s).";
"login.receipt.button" = "Login using purchase receipt";
"login.magic.link.title" = "Login using magic email link";
"login.magic.link.response" = "Please check your e-mail for a login link.";
Expand Down
4 changes: 2 additions & 2 deletions PIALibrary/Sources/Library/ClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import Foundation

/// All the errors raised by the client.
public enum ClientError: String, Error {
public enum ClientError: Error, Equatable {

/// The Internet is unreachable.
case internetUnreachable
Expand All @@ -32,7 +32,7 @@ public enum ClientError: String, Error {
case unauthorized

/// The service has been throttled for exceeded rate limits.
case throttled
case throttled(retryAfter: UInt)

/// The service has been expired.
case expired
Expand Down
23 changes: 21 additions & 2 deletions PIALibrary/Sources/Library/WebServices/PIAWebServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,12 @@ class PIAWebServices: WebServices, ConfigurationAccess {
}

private func mapLoginError(_ error: AccountRequestError) -> ClientError {
switch error.code {
case 429:
return .throttled(retryAfter: UInt(error.retryAfterSeconds))
default:
return .unauthorized
}
}


Expand All @@ -183,7 +188,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {

private func mapLoginLinkError(_ error:AccountRequestError) -> ClientError {
switch error.code {
case 401,402:
case 401,402,429:
return mapLoginError(error)
default:
return .invalidParameter
Expand Down Expand Up @@ -281,10 +286,24 @@ class PIAWebServices: WebServices, ConfigurationAccess {
}
}

fileprivate func mapDIPError(_ error: AccountRequestError?) -> ClientError {
guard let error = error else {
return ClientError.invalidParameter
}
switch error.code {
case 401:
return ClientError.unauthorized
case 429:
return ClientError.throttled(retryAfter: UInt(error.retryAfterSeconds))
default:
return ClientError.invalidParameter
}
}

func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?) {
self.accountAPI.dedicatedIPs(ipTokens: tokens) { (dedicatedIps, errors) in
if !errors.isEmpty {
callback?([], errors.last?.code == 401 ? ClientError.unauthorized : ClientError.invalidParameter)
callback?([], self.mapDIPError(errors.last))
return
}

Expand Down
6 changes: 4 additions & 2 deletions PIALibrary/Sources/UI/iOS/SwiftGen+Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,10 @@ internal enum L10n {
/// Sign in to your account
internal static let title = L10n.tr("Welcome", "login.title")
internal enum Error {
/// Too many failed login attempts with this username. Please try again later.
internal static let throttled = L10n.tr("Welcome", "login.error.throttled")
/// Too many failed login attempts with this username. Please try again after %@ second(s).
internal static func throttled(_ p1: Any) -> String {
return L10n.tr("Welcome", "login.error.throttled", String(describing: p1))
}
/// Log in
internal static let title = L10n.tr("Welcome", "login.error.title")
/// Your username or password is incorrect.
Expand Down
76 changes: 59 additions & 17 deletions PIALibrary/Sources/UI/iOS/ViewControllers/LoginViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ private let log = SwiftyBeaver.self

class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeViewControllerDelegate {

private enum LoginOption {
case credentials
case receipt
case magicLink
}

@IBOutlet private weak var scrollView: UIScrollView!

@IBOutlet private weak var labelTitle: UILabel!
Expand Down Expand Up @@ -54,6 +60,10 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie

private var isLogging = false

private var timeToRetryCredentials: TimeInterval? = nil
private var timeToRetryReceipt: TimeInterval? = nil
private var timeToRetryMagicLink: TimeInterval? = nil

deinit {
NotificationCenter.default.removeObserver(self)
}
Expand Down Expand Up @@ -111,6 +121,10 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
}
// MARK: Actions
@IBAction private func logInWithLink(_ sender: Any?) {
if let timeUntilNextTry = timeToRetryMagicLink?.timeSinceNow() {
displayErrorMessage(errorMessage: L10n.Welcome.Login.Error.throttled("\(Int(timeUntilNextTry))"), displayDuration: timeUntilNextTry)
return
}

let bundle = Bundle(for: LoginViewController.self)
let storyboard = UIStoryboard(name: "Welcome", bundle: bundle)
Expand All @@ -131,19 +145,16 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
}

self.showLoadingAnimation()

self.preset?.accountProvider.loginUsingMagicLink(withEmail: email, { (error) in

self.hideLoadingAnimation()
guard error == nil else {
self.handleLoginFailed(error)
self.handleLoginFailed(error, loginOption: .magicLink)
return
}

Macros.displaySuccessImageNote(withImage: Asset.iconWarning.image,
message: L10n.Welcome.Login.Magic.Link.response)

self.hideLoadingAnimation()

})

})
Expand All @@ -167,6 +178,10 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
}

@IBAction private func logInWithReceipt(_ sender: Any?) {
if let timeUntilNextTry = timeToRetryReceipt?.timeSinceNow() {
displayErrorMessage(errorMessage: L10n.Welcome.Login.Error.throttled("\(Int(timeUntilNextTry))"), displayDuration: timeUntilNextTry)
return
}

guard !isLogging else {
return
Expand All @@ -179,11 +194,17 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
let request = LoginReceiptRequest(receipt: receipt)

prepareLogin()
preset?.accountProvider.login(with: request, handleLoginResult)
preset?.accountProvider.login(with: request, { userAccount, error in
self.handleLoginResult(user: userAccount, error: error, loginOption: .receipt)
})
}

@IBAction private func logIn(_ sender: Any?) {

if let timeUntilNextTry = timeToRetryCredentials?.timeSinceNow() {
displayErrorMessage(errorMessage: L10n.Welcome.Login.Error.throttled("\(Int(timeUntilNextTry))"), displayDuration: timeUntilNextTry)
return
}

guard !isLogging else {
return
}
Expand All @@ -195,7 +216,9 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
let request = LoginRequest(credentials: credentials)

prepareLogin()
preset?.accountProvider.login(with: request, handleLoginResult)
preset?.accountProvider.login(with: request, { userAccount, error in
self.handleLoginResult(user: userAccount, error: error, loginOption: .credentials)
})
}

private func getValidCredentials() -> Credentials? {
Expand Down Expand Up @@ -247,13 +270,13 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
showLoadingAnimation()
}

private func handleLoginResult(user: UserAccount?, error: Error?) {
private func handleLoginResult(user: UserAccount?, error: Error?, loginOption: LoginOption) {
enableInteractions(true)

hideLoadingAnimation()

guard let user = user else {
handleLoginFailed(error)
handleLoginFailed(error, loginOption: loginOption)
return
}

Expand All @@ -262,16 +285,36 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
self.completionDelegate?.welcomeDidLogin(withUser: user, topViewController: self)
}

private func handleLoginFailed(_ error: Error?) {
private func updateTimeToRetry(loginOption: LoginOption, retryAfterSeconds: Double) {
let retryAfterTimeStamp = Date().timeIntervalSince1970 + retryAfterSeconds
switch loginOption {
case .credentials:
timeToRetryCredentials = retryAfterTimeStamp
case .receipt:
timeToRetryReceipt = retryAfterTimeStamp
case .magicLink:
timeToRetryMagicLink = retryAfterTimeStamp
}
}

private func handleLoginFailed(_ error: Error?, loginOption: LoginOption) {
var displayDuration: Double?
var errorMessage: String?
if let error = error {
if let clientError = error as? ClientError {
switch clientError {
case .unauthorized:
errorMessage = L10n.Welcome.Login.Error.unauthorized

case .throttled:
errorMessage = L10n.Welcome.Login.Error.throttled
case .throttled(retryAfter: let retryAfter):
let localisedThrottlingString = L10n.Welcome.Login.Error.throttled("\(retryAfter)")
errorMessage = NSLocalizedString(localisedThrottlingString, comment: localisedThrottlingString)

let retryAfterSeconds = Double(retryAfter)
displayDuration = retryAfterSeconds

updateTimeToRetry(loginOption: loginOption, retryAfterSeconds: retryAfterSeconds)

case .expired:
handleExpiredAccount()
return
Expand All @@ -286,14 +329,13 @@ class LoginViewController: AutolayoutViewController, WelcomeChild, PIAWelcomeVie
} else {
log.error("Failed to log in")
}

displayErrorMessage(errorMessage: errorMessage)
displayErrorMessage(errorMessage: errorMessage, displayDuration: displayDuration)
}

private func displayErrorMessage(errorMessage: String?) {
private func displayErrorMessage(errorMessage: String?, displayDuration: Double? = nil) {

Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: errorMessage ?? L10n.Welcome.Login.Error.title)
message: errorMessage ?? L10n.Welcome.Login.Error.title, andDuration: displayDuration)
}

private func handleExpiredAccount() {
Expand Down
17 changes: 17 additions & 0 deletions PIALibrary/Sources/Util/TimeInterval+Date.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// TimeInterval+Date.swift
// PIALibrary
//
// Created by Waleed Mahmood on 04.02.22.
//

import Foundation

public extension TimeInterval {
/// Returns time in seconds between receiver & now
/// and a nil if now has already gone ahead of the receiver.
public func timeSinceNow() -> Double? {
let now = Date().timeIntervalSince1970
return now > self ? nil : self - now
}
}

0 comments on commit 1d95c91

Please sign in to comment.