Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magic Links: Handling 429 #1618

Merged
merged 12 commits into from
Jul 17, 2024
3 changes: 2 additions & 1 deletion Simplenote/AuthenticationMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ extension AuthenticationMode {

/// Login with Password
///
static var loginWithPassword: AuthenticationMode {
static func loginWithPassword(header: String? = nil) -> AuthenticationMode {
return .init(title: NSLocalizedString("Log In with Password", comment: "LogIn Interface Title"),
header: header,
inputElements: [.password],
validationStyle: .legacy,
actions: [
Expand Down
3 changes: 2 additions & 1 deletion Simplenote/Classes/SPAuthError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum SPAuthError: Error {
case compromisedPassword
case unverifiedEmail
case tooManyAttempts
case generic
case unknown(statusCode: Int, response: String?, error: Error?)
}

Expand Down Expand Up @@ -89,7 +90,7 @@ extension SPAuthError {
return NSLocalizedString("You must verify your email before being able to login.", comment: "Error for un verified email")
case .tooManyAttempts:
return NSLocalizedString("Too many login attempts. Try again later.", comment: "Error message for too many login attempts")
case .unknown:
default:
return NSLocalizedString("We're having problems. Please try again soon.", comment: "Generic error")
}
}
Expand Down
35 changes: 24 additions & 11 deletions Simplenote/Classes/SPAuthViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,13 +435,16 @@ extension SPAuthViewController {
lockdownInterface()

controller.requestLoginEmail(username: email) { error in
// TODO: 429 (Rate Limited)? push PW auth instead

if let error {
self.handleError(error: error)
} else {
switch error {
case .none:
self.presentCodeInterface()
SPTracker.trackLoginLinkRequested()

case .tooManyAttempts:
self.presentPasswordInterfaceWithRateLimitingHeader()

case .some(let error):
self.handleError(error: error)
}

self.unlockInterface()
Expand All @@ -459,11 +462,12 @@ extension SPAuthViewController {
do {
try await controller.loginWithCode(username: state.username, code: state.code)
SPTracker.trackLoginLinkConfirmationSuccess()
} catch let error as SPAuthError {
SPTracker.trackLoginLinkConfirmationFailure()
self.handleError(error: error)
} catch {
// TODO: Fixme
/// Errors will always be of the `SPAuthError` type. Let's switch to Typed Errors, as soon as we migrate over to Xcode 16
let error = error as? SPAuthError ?? .generic
self.handleError(error: error)

SPTracker.trackLoginLinkConfirmationFailure()
}

unlockInterface()
Expand Down Expand Up @@ -511,7 +515,15 @@ extension SPAuthViewController {
}

@IBAction func presentPasswordInterface() {
let viewController = SPAuthViewController(controller: controller, mode: .loginWithPassword, state: state)
presentPasswordInterfaceWithHeader(header: nil)
}

@IBAction func presentPasswordInterfaceWithRateLimitingHeader() {
presentPasswordInterfaceWithHeader(header: AuthenticationStrings.loginWithEmailLimitHeader)
}

@IBAction func presentPasswordInterfaceWithHeader(header: String?) {
let viewController = SPAuthViewController(controller: controller, mode: .loginWithPassword(header: header), state: state)
navigationController?.pushViewController(viewController, animated: true)
}
}
Expand Down Expand Up @@ -674,7 +686,7 @@ private extension SPAuthViewController {
}

// Prefill the LoginViewController
let loginViewController = SPAuthViewController(controller: controller, mode: .loginWithPassword, state: state)
let loginViewController = SPAuthViewController(controller: controller, mode: .loginWithPassword(), state: state)
loginViewController.loadViewIfNeeded()

// Swap the current VC
Expand Down Expand Up @@ -839,6 +851,7 @@ private enum AuthenticationStrings {
static let acceptActionText = NSLocalizedString("Accept", comment: "Accept Action")
static let cancelActionText = NSLocalizedString("Cancel", comment: "Cancel Action")
static let loginActionText = NSLocalizedString("Log In", comment: "Log In Action")
static let loginWithEmailLimitHeader = NSLocalizedString("Log in with email failed, please enter your password", comment: "Header for Enter Password UI, when the user performed too many requests")
static let compromisedAlertCancel = NSLocalizedString("Cancel", comment: "Cancel action for password alert")
static let compromisedAlertReset = NSLocalizedString("Change Password", comment: "Change password action")
static let unverifiedCancelText = NSLocalizedString("Ok", comment: "Email unverified alert dismiss")
Expand Down
38 changes: 34 additions & 4 deletions SimplenoteUITests/EmailLogin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,34 @@ class EmailLogin {
}

class func close() {
/// Back from Password > Email UI
/// Exit: We're already in the Onboarding UI
///
if app.buttons[UID.Button.logIn].exists, app.buttons[UID.Button.signUp].exists {
return
}

/// Back from Password > Code UI
///
let backFromPasswordUI = app.navigationBars[UID.NavBar.logInWithPassword].buttons.element(boundBy: 0)
if backFromPasswordUI.exists {
backFromPasswordUI.tap()
_ = app.navigationBars[UID.NavBar.enterCode].waitForExistence(timeout: minLoadTimeout)
}

/// Back from Code UI > Email UI
/// Important: When rate-limited, the Code UI is skipped
///
let codeNavigationBar = app.navigationBars[UID.NavBar.enterCode]
if codeNavigationBar.exists {
codeNavigationBar.buttons.element(boundBy: 0).tap()
_ = app.navigationBars[UID.NavBar.logIn].waitForExistence(timeout: minLoadTimeout)
}

/// Back from Email UI > Onboarding
let backButton = app.navigationBars[UID.NavBar.logIn].buttons.element(boundBy: 0)
if backButton.isHittable {
backButton.tap()
///
let emailNavigationBar = app.navigationBars[UID.NavBar.logIn]
if emailNavigationBar.exists {
emailNavigationBar.buttons.element(boundBy: 0).tap()
}

handleSavePasswordPrompt()
Expand All @@ -42,6 +59,19 @@ class EmailLogin {
class func logIn(email: String, password: String) {
enterEmail(enteredValue: email)
app.buttons[UID.Button.logInWithEmail].tap()

/// Code UI > Password UI
/// Important: When rate-limited, the Code UI is skipped
///
let codeNavigationBar = app.navigationBars[UID.NavBar.enterCode]
_ = codeNavigationBar.waitForExistence(timeout: minLoadTimeout)

if codeNavigationBar.exists {
app.buttons[UID.Button.enterPassword].tap()
}

/// Password UI
///
_ = app.buttons[UID.Button.logIn].waitForExistence(timeout: minLoadTimeout)
enterPassword(enteredValue: password)
app.buttons[UID.Button.mainAction].tap()
Expand Down
2 changes: 2 additions & 0 deletions SimplenoteUITests/UIDs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum UID {
static let allNotes = "All Notes"
static let logIn = "Log In"
static let logInWithPassword = "Log In with Password"
static let enterCode = "Enter Code"
static let noteEditorPreview = "Preview"
static let noteEditorOptions = "Options"
static let trash = "Trash"
Expand All @@ -27,6 +28,7 @@ enum UID {
static let menu = "menu"
static let signUp = "Sign Up"
static let logIn = "Log In"
static let enterPassword = "Enter password"
static let mainAction = "Main Action"
static let logInWithEmail = "Log in with email"
static let allNotes = "All Notes"
Expand Down
Loading