From a65185f5e631f84e4825db33bf375b0e27485c68 Mon Sep 17 00:00:00 2001 From: Martin Walsh Date: Mon, 27 Mar 2017 09:42:43 +0100 Subject: [PATCH 1/4] Refactored UnrecoverableError View Unrecoverable assets added Added i18n Strings Update Tests --- Lock.xcodeproj/project.pbxproj | 2 +- Lock/Base.lproj/Lock.strings | 12 ++- .../Contents.json | 12 +++ .../ic_connection_error.pdf | Bin 0 -> 4491 bytes Lock/UnrecoverableError.swift | 9 ++ Lock/UnrecoverableErrorPresenter.swift | 14 ++- Lock/UnrecoverableErrorView.swift | 88 ++++++++++++------ .../UnrecoverableErrorPresenterSpec.swift | 53 +++++++++-- 8 files changed, 145 insertions(+), 45 deletions(-) create mode 100644 Lock/Lock.xcassets/ic_connection_error.imageset/Contents.json create mode 100644 Lock/Lock.xcassets/ic_connection_error.imageset/ic_connection_error.pdf diff --git a/Lock.xcodeproj/project.pbxproj b/Lock.xcodeproj/project.pbxproj index f66d3cb7c..2b4f5df14 100644 --- a/Lock.xcodeproj/project.pbxproj +++ b/Lock.xcodeproj/project.pbxproj @@ -522,8 +522,8 @@ 5BB4A7C01DF9A38E008E8C37 /* DatabaseView.swift */, 5F1C49921D8360DF005B74FC /* LoadingView.swift */, 5B55F3C81E24273D00B75CF5 /* UnrecoverableErrorView.swift */, - 5F73CDD31D3073BE00D8D8D1 /* DatabaseForgotPasswordView.swift */, 5B5F9F9E1E4B3FBE00EAB9EE /* PasswordlessView.swift */, + 5F73CDD31D3073BE00D8D8D1 /* DatabaseForgotPasswordView.swift */, 5B0971811DC8FAC5003AA88F /* EnterpriseDomainView.swift */, 5B4DE0181DD670F7004C8AC2 /* EnterpriseActiveAuthView.swift */, 5F50900D1D1DF40400EAA650 /* DatabaseOnlyView.swift */, diff --git a/Lock/Base.lproj/Lock.strings b/Lock/Base.lproj/Lock.strings index bedb6cd10..c0fa00c15 100644 --- a/Lock/Base.lproj/Lock.strings +++ b/Lock/Base.lproj/Lock.strings @@ -88,6 +88,16 @@ "com.auth0.lock.error.unrecoverable.invalid_options" = "Your options configuration failed with: %1$@"; // No connections "com.auth0.lock.error.unrecoverable.no_connections" = "No authentication methods found for this client. please check your client setup."; +// Retry action +"com.auth0.lock.error.unrecoverable.retry.action" = "retry."; +// Retry label +"com.auth0.lock.error.unrecoverable.retry.title" = "Please "; +// Support action +"com.auth0.lock.error.unrecoverable.support.action" = "support."; +// Support label +"com.auth0.lock.error.unrecoverable.support.title" = "Please contact "; +// Unrecoverable error title +"com.auth0.lock.error.unrecoverable.title" = "We encountered an error"; // Forgot Password message "com.auth0.lock.forgot.message" = "Please enter your email and the new password. We will send you an email to confirm the password change."; // Forgot Password title @@ -208,8 +218,6 @@ "com.auth0.lock.strategy.signup.title" = "SIGN UP WITH %1$@"; // Login Button title "com.auth0.lock.submit.login.title" = "LOG IN"; -// Retry -"com.auth0.lock.submit.retry.title" = "RETRY"; // Send 2fa code "com.auth0.lock.submit.send_code.title" = "SEND"; // Send Email button title diff --git a/Lock/Lock.xcassets/ic_connection_error.imageset/Contents.json b/Lock/Lock.xcassets/ic_connection_error.imageset/Contents.json new file mode 100644 index 000000000..295f3237c --- /dev/null +++ b/Lock/Lock.xcassets/ic_connection_error.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_connection_error.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Lock/Lock.xcassets/ic_connection_error.imageset/ic_connection_error.pdf b/Lock/Lock.xcassets/ic_connection_error.imageset/ic_connection_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..faf8190d98d24d3e8d8dfb9b7de6c67e61d79ed7 GIT binary patch literal 4491 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 literal 0 HcmV?d00001 diff --git a/Lock/UnrecoverableError.swift b/Lock/UnrecoverableError.swift index 56d6e01b7..be44bbe8d 100644 --- a/Lock/UnrecoverableError.swift +++ b/Lock/UnrecoverableError.swift @@ -42,6 +42,15 @@ enum UnrecoverableError: Equatable, Error { return "Something went wrong.\nPlease contact technical support.".i18n(key: "com.auth0.lock.error.unrecoverable.default", comment: "Default error") } } + + var canRetry: Bool { + switch self { + case .connectionTimeout, .requestIssue: + return true + default: + return false + } + } } func == (lhs: UnrecoverableError, rhs: UnrecoverableError) -> Bool { diff --git a/Lock/UnrecoverableErrorPresenter.swift b/Lock/UnrecoverableErrorPresenter.swift index 340395b4c..7908077d2 100644 --- a/Lock/UnrecoverableErrorPresenter.swift +++ b/Lock/UnrecoverableErrorPresenter.swift @@ -22,7 +22,7 @@ import Foundation -class UnrecoverableErrorPresenter: Presentable, Loggable { +class UnrecoverableErrorPresenter: Presentable { let navigator: Navigable let error: UnrecoverableError @@ -34,9 +34,15 @@ class UnrecoverableErrorPresenter: Presentable, Loggable { } var view: View { - let view = UnrecoverableErrorView(message: self.error.localizableMessage) - view.primaryButton?.onPress = { _ in - self.navigator.navigate(.root) + let view = UnrecoverableErrorView(canRetry: self.error.canRetry) + if self.error.canRetry { + view.secondaryButton?.onPress = { _ in + self.navigator.navigate(.root) + } + } else { + view.secondaryButton?.onPress = { _ in + UIApplication.shared.openURL(URL(string: "https://auth0.com/docs/")!) + } } return view } diff --git a/Lock/UnrecoverableErrorView.swift b/Lock/UnrecoverableErrorView.swift index 30efc5de2..939db605a 100644 --- a/Lock/UnrecoverableErrorView.swift +++ b/Lock/UnrecoverableErrorView.swift @@ -24,43 +24,72 @@ import UIKit class UnrecoverableErrorView: UIView, View { - weak var primaryButton: PrimaryButton? - weak var label: UILabel? + weak var secondaryButton: SecondaryButton? - init(message: String) { - let primaryButton = PrimaryButton() + init(canRetry: Bool) { let center = UILayoutGuide() - let label = UILabel() - - self.primaryButton = primaryButton - self.label = label + let messageLabel = UILabel() + let imageView = UIImageView() + let actionLabel = UILabel() + let actionButton = SecondaryButton() + let actionView = UIView() + self.secondaryButton = actionButton super.init(frame: CGRect.zero) - self.addSubview(primaryButton) - self.addSubview(label) + self.addSubview(imageView) + self.addSubview(messageLabel) + self.addSubview(actionView) self.addLayoutGuide(center) - constraintEqual(anchor: center.leftAnchor, toAnchor: self.leftAnchor, constant: 20) + actionView.addSubview(actionLabel) + actionView.addSubview(actionButton) + + constraintEqual(anchor: center.leftAnchor, toAnchor: self.leftAnchor) constraintEqual(anchor: center.topAnchor, toAnchor: self.topAnchor) - constraintEqual(anchor: center.rightAnchor, toAnchor: self.rightAnchor, constant: -20) - constraintEqual(anchor: center.bottomAnchor, toAnchor: primaryButton.topAnchor) - - constraintEqual(anchor: label.leftAnchor, toAnchor: center.leftAnchor) - constraintEqual(anchor: label.rightAnchor, toAnchor: center.rightAnchor) - constraintEqual(anchor: label.centerYAnchor, toAnchor: center.centerYAnchor, constant: -20) - label.translatesAutoresizingMaskIntoConstraints = false - - constraintEqual(anchor: primaryButton.leftAnchor, toAnchor: self.leftAnchor) - constraintEqual(anchor: primaryButton.rightAnchor, toAnchor: self.rightAnchor) - constraintEqual(anchor: primaryButton.bottomAnchor, toAnchor: self.bottomAnchor) - primaryButton.translatesAutoresizingMaskIntoConstraints = false - - label.text = message - label.textAlignment = .center - label.numberOfLines = 3 - label.font = mediumSystemFont(size: 16) - primaryButton.title = "RETRY".i18n(key: "com.auth0.lock.submit.retry.title", comment: "Retry") + constraintEqual(anchor: center.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: center.bottomAnchor, toAnchor: self.bottomAnchor) + + constraintEqual(anchor: imageView.centerXAnchor, toAnchor: center.centerXAnchor) + constraintEqual(anchor: imageView.centerYAnchor, toAnchor: center.centerYAnchor, constant: -90) + imageView.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: messageLabel.leftAnchor, toAnchor: self.leftAnchor) + constraintEqual(anchor: messageLabel.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: messageLabel.centerYAnchor, toAnchor: center.centerYAnchor) + messageLabel.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: actionView.centerXAnchor, toAnchor: center.centerXAnchor) + constraintEqual(anchor: actionView.centerYAnchor, toAnchor: center.centerYAnchor, constant: 50) + dimension(dimension: actionView.heightAnchor, withValue: 50) + actionView.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: actionLabel.leftAnchor, toAnchor: actionView.leftAnchor) + constraintEqual(anchor: actionLabel.centerYAnchor, toAnchor: actionView.centerYAnchor) + constraintEqual(anchor: actionLabel.rightAnchor, toAnchor: actionButton.leftAnchor) + actionLabel.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: actionButton.rightAnchor, toAnchor: actionView.rightAnchor) + constraintEqual(anchor: actionButton.leftAnchor, toAnchor: actionLabel.rightAnchor) + constraintEqual(anchor: actionButton.centerYAnchor, toAnchor: actionView.centerYAnchor) + actionButton.translatesAutoresizingMaskIntoConstraints = false + + imageView.image = LazyImage(name: "ic_connection_error", bundle: bundleForLock()).image(compatibleWithTraits: self.traitCollection) + messageLabel.text = "We encountered an error".i18n(key: "com.auth0.lock.error.unrecoverable.title", comment: "Unrecoverable error title") + messageLabel.textAlignment = .center + messageLabel.font = lightSystemFont(size: 24) + actionLabel.textColor = UIColor.lightGray + actionLabel.font = regularSystemFont(size: 16) + actionButton.button?.setTitleColor(UIColor(red:0.04, green:0.53, blue:0.69, alpha:1.0), for: .normal) + actionButton.button?.titleLabel?.font = actionLabel.font + + if canRetry { + actionLabel.text = "Please ".i18n(key: "com.auth0.lock.error.unrecoverable.retry.title", comment: "Retry label") + actionButton.title = "retry.".i18n(key: "com.auth0.lock.error.unrecoverable.retry.action", comment: "Retry action") + } else { + actionLabel.text = "Please contact ".i18n(key: "com.auth0.lock.error.unrecoverable.support.title", comment: "Support label") + actionButton.title = "support.".i18n(key: "com.auth0.lock.error.unrecoverable.support.action", comment: "Support action") + } } required init?(coder aDecoder: NSCoder) { @@ -68,6 +97,5 @@ class UnrecoverableErrorView: UIView, View { } func apply(style: Style) { - self.primaryButton?.apply(style: style) } } diff --git a/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift b/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift index c9533313b..8554891f5 100644 --- a/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift +++ b/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift @@ -48,23 +48,60 @@ class UnrecoverableErrorPresenterSpec: QuickSpec { expect(presenter.view as? UnrecoverableErrorView).toNot(beNil()) } - it("should have button title") { - expect(view.primaryButton?.title) == "RETRY" + context("retry error") { + + beforeEach { + presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator) + view = presenter.view as? UnrecoverableErrorView + } + + it("should have relevant retry button title") { + expect(view.secondaryButton?.title?.contains("retry")) == true + } } - it("should display relevant error message") { - expect(view.label?.text) == error.localizableMessage + context("support error") { + + beforeEach { + presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator) + view = presenter.view as? UnrecoverableErrorView + } + + it("should have relevant support button title") { + expect(view.secondaryButton?.title?.contains("support")) == true + } } } describe("action") { - it("should trigger retry on button press") { - view.primaryButton?.onPress(view.primaryButton!) - expect(navigator.route) == Route.root + context("retry error") { + + beforeEach { + presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator) + view = presenter.view as? UnrecoverableErrorView + } + + it("should trigger retry on button press") { + view.secondaryButton?.onPress(view.secondaryButton!) + expect(navigator.route) == Route.root + } } - } + context("support error") { + + beforeEach { + presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator) + view = presenter.view as? UnrecoverableErrorView + } + it("should not trigger retry on button press") { + view.secondaryButton?.onPress(view.secondaryButton!) + expect(navigator.route).to(beNil()) + } + } + + } + } } From c55b1b5a5b1f3a230677240ef36fcbb85c5ac579 Mon Sep 17 00:00:00 2001 From: Martin Walsh Date: Mon, 3 Apr 2017 11:12:50 +0100 Subject: [PATCH 2/4] Updated Design Simplified UnrecoverableErrorView Added optional 'supportURL' to be displayed on non recoverable errors Added Options Tests Updated UnrecoverableError Tests --- Lock/Base.lproj/Lock.strings | 18 ++++---- Lock/LockOptions.swift | 1 + Lock/OptionBuildable.swift | 15 +++++++ Lock/Options.swift | 1 + Lock/Router.swift | 3 +- Lock/UnrecoverableErrorPresenter.swift | 9 ++-- Lock/UnrecoverableErrorView.swift | 43 ++++++------------- LockTests/OptionsSpec.swift | 18 ++++++++ .../UnrecoverableErrorPresenterSpec.swift | 37 ++++++++++++---- 9 files changed, 92 insertions(+), 53 deletions(-) diff --git a/Lock/Base.lproj/Lock.strings b/Lock/Base.lproj/Lock.strings index c0fa00c15..ca3d0dd62 100644 --- a/Lock/Base.lproj/Lock.strings +++ b/Lock/Base.lproj/Lock.strings @@ -88,16 +88,14 @@ "com.auth0.lock.error.unrecoverable.invalid_options" = "Your options configuration failed with: %1$@"; // No connections "com.auth0.lock.error.unrecoverable.no_connections" = "No authentication methods found for this client. please check your client setup."; -// Retry action -"com.auth0.lock.error.unrecoverable.retry.action" = "retry."; -// Retry label -"com.auth0.lock.error.unrecoverable.retry.title" = "Please "; -// Support action -"com.auth0.lock.error.unrecoverable.support.action" = "support."; -// Support label -"com.auth0.lock.error.unrecoverable.support.title" = "Please contact "; -// Unrecoverable error title -"com.auth0.lock.error.unrecoverable.title" = "We encountered an error"; +// Retry button title +"com.auth0.lock.error.unrecoverable.retry.button" = "Retry request"; +// Unrecoverable error retry title +"com.auth0.lock.error.unrecoverable.retry.title" = "There was a problem with the request."; +// Support button title +"com.auth0.lock.error.unrecoverable.support.button" = "Contact support"; +// Unrecoverable error support title +"com.auth0.lock.error.unrecoverable.support.title" = "There was an unexpected error, please contact support."; // Forgot Password message "com.auth0.lock.forgot.message" = "Please enter your email and the new password. We will send you an email to confirm the password change."; // Forgot Password title diff --git a/Lock/LockOptions.swift b/Lock/LockOptions.swift index 17bef812b..9c3293b13 100644 --- a/Lock/LockOptions.swift +++ b/Lock/LockOptions.swift @@ -27,6 +27,7 @@ struct LockOptions: OptionBuildable { var closable: Bool = false var termsOfServiceURL: URL = URL(string: "https://auth0.com/terms")! var privacyPolicyURL: URL = URL(string: "https://auth0.com/privacy")! + var supportURL: URL? var logLevel: LoggerLevel = .off var loggerOutput: LoggerOutput? var logHttpRequest: Bool = false diff --git a/Lock/OptionBuildable.swift b/Lock/OptionBuildable.swift index fc3c15df9..159e3d724 100644 --- a/Lock/OptionBuildable.swift +++ b/Lock/OptionBuildable.swift @@ -36,6 +36,9 @@ public protocol OptionBuildable: Options { /// Privacy Policy URL. By default is Auth0's. var privacyPolicyURL: URL { get set } + /// Support page url that will be displayed (Inside Safari) when an unrecoverable error occurs and the user taps the "Contact Support" button in the error screen. + var supportURL: URL? { get set } + /// Log level for Lock. By default is `Off`. var logLevel: LoggerLevel { get set } @@ -136,4 +139,16 @@ public extension OptionBuildable { } } + /// Support Page URL. By default is not set. + var supportPage: String? { + get { + guard let url = self.supportURL else { return nil } + return url.absoluteString + } + set { + guard let value = newValue, let url = URL(string: value) else { return } // FIXME: log error + self.supportURL = url + } + } + } diff --git a/Lock/Options.swift b/Lock/Options.swift index 554bdb68a..1e5bc6805 100644 --- a/Lock/Options.swift +++ b/Lock/Options.swift @@ -27,6 +27,7 @@ public protocol Options { var termsOfServiceURL: URL { get } var privacyPolicyURL: URL { get } + var supportURL: URL? { get } var logLevel: LoggerLevel { get } var loggerOutput: LoggerOutput? { get } diff --git a/Lock/Router.swift b/Lock/Router.swift index 6f0632ab8..4bc841504 100644 --- a/Lock/Router.swift +++ b/Lock/Router.swift @@ -57,7 +57,8 @@ extension Router { } func unrecoverableError(for error: UnrecoverableError) -> Presentable? { - let presenter = UnrecoverableErrorPresenter(error: error, navigator: self) + guard let options = self.controller?.lock.options else { return nil } + let presenter = UnrecoverableErrorPresenter(error: error, navigator: self, options: options) return presenter } } diff --git a/Lock/UnrecoverableErrorPresenter.swift b/Lock/UnrecoverableErrorPresenter.swift index 7908077d2..8d83b9cdd 100644 --- a/Lock/UnrecoverableErrorPresenter.swift +++ b/Lock/UnrecoverableErrorPresenter.swift @@ -25,12 +25,14 @@ import Foundation class UnrecoverableErrorPresenter: Presentable { let navigator: Navigable let error: UnrecoverableError + let options: Options var messagePresenter: MessagePresenter? - init(error: UnrecoverableError, navigator: Navigable) { + init(error: UnrecoverableError, navigator: Navigable, options: Options) { self.navigator = navigator self.error = error + self.options = options } var view: View { @@ -39,10 +41,11 @@ class UnrecoverableErrorPresenter: Presentable { view.secondaryButton?.onPress = { _ in self.navigator.navigate(.root) } - } else { + } else if let supportURL = self.options.supportURL { view.secondaryButton?.onPress = { _ in - UIApplication.shared.openURL(URL(string: "https://auth0.com/docs/")!) + UIApplication.shared.openURL(supportURL) } + view.secondaryButton?.isHidden = false } return view } diff --git a/Lock/UnrecoverableErrorView.swift b/Lock/UnrecoverableErrorView.swift index 939db605a..1e3e14002 100644 --- a/Lock/UnrecoverableErrorView.swift +++ b/Lock/UnrecoverableErrorView.swift @@ -30,21 +30,16 @@ class UnrecoverableErrorView: UIView, View { let center = UILayoutGuide() let messageLabel = UILabel() let imageView = UIImageView() - let actionLabel = UILabel() let actionButton = SecondaryButton() - let actionView = UIView() self.secondaryButton = actionButton super.init(frame: CGRect.zero) self.addSubview(imageView) self.addSubview(messageLabel) - self.addSubview(actionView) + self.addSubview(actionButton) self.addLayoutGuide(center) - actionView.addSubview(actionLabel) - actionView.addSubview(actionButton) - constraintEqual(anchor: center.leftAnchor, toAnchor: self.leftAnchor) constraintEqual(anchor: center.topAnchor, toAnchor: self.topAnchor) constraintEqual(anchor: center.rightAnchor, toAnchor: self.rightAnchor) @@ -54,41 +49,29 @@ class UnrecoverableErrorView: UIView, View { constraintEqual(anchor: imageView.centerYAnchor, toAnchor: center.centerYAnchor, constant: -90) imageView.translatesAutoresizingMaskIntoConstraints = false - constraintEqual(anchor: messageLabel.leftAnchor, toAnchor: self.leftAnchor) - constraintEqual(anchor: messageLabel.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: messageLabel.leftAnchor, toAnchor: self.leftAnchor, constant: 20) + constraintEqual(anchor: messageLabel.rightAnchor, toAnchor: self.rightAnchor, constant: -20) constraintEqual(anchor: messageLabel.centerYAnchor, toAnchor: center.centerYAnchor) messageLabel.translatesAutoresizingMaskIntoConstraints = false - constraintEqual(anchor: actionView.centerXAnchor, toAnchor: center.centerXAnchor) - constraintEqual(anchor: actionView.centerYAnchor, toAnchor: center.centerYAnchor, constant: 50) - dimension(dimension: actionView.heightAnchor, withValue: 50) - actionView.translatesAutoresizingMaskIntoConstraints = false - - constraintEqual(anchor: actionLabel.leftAnchor, toAnchor: actionView.leftAnchor) - constraintEqual(anchor: actionLabel.centerYAnchor, toAnchor: actionView.centerYAnchor) - constraintEqual(anchor: actionLabel.rightAnchor, toAnchor: actionButton.leftAnchor) - actionLabel.translatesAutoresizingMaskIntoConstraints = false - - constraintEqual(anchor: actionButton.rightAnchor, toAnchor: actionView.rightAnchor) - constraintEqual(anchor: actionButton.leftAnchor, toAnchor: actionLabel.rightAnchor) - constraintEqual(anchor: actionButton.centerYAnchor, toAnchor: actionView.centerYAnchor) + constraintEqual(anchor: actionButton.centerXAnchor, toAnchor: center.centerXAnchor) + constraintEqual(anchor: actionButton.centerYAnchor, toAnchor: center.centerYAnchor, constant: 70) actionButton.translatesAutoresizingMaskIntoConstraints = false imageView.image = LazyImage(name: "ic_connection_error", bundle: bundleForLock()).image(compatibleWithTraits: self.traitCollection) - messageLabel.text = "We encountered an error".i18n(key: "com.auth0.lock.error.unrecoverable.title", comment: "Unrecoverable error title") messageLabel.textAlignment = .center - messageLabel.font = lightSystemFont(size: 24) - actionLabel.textColor = UIColor.lightGray - actionLabel.font = regularSystemFont(size: 16) + messageLabel.font = lightSystemFont(size: 22) + messageLabel.numberOfLines = 3 actionButton.button?.setTitleColor(UIColor(red:0.04, green:0.53, blue:0.69, alpha:1.0), for: .normal) - actionButton.button?.titleLabel?.font = actionLabel.font + actionButton.button?.titleLabel?.font = regularSystemFont(size: 16) if canRetry { - actionLabel.text = "Please ".i18n(key: "com.auth0.lock.error.unrecoverable.retry.title", comment: "Retry label") - actionButton.title = "retry.".i18n(key: "com.auth0.lock.error.unrecoverable.retry.action", comment: "Retry action") + messageLabel.text = "There was a problem with the request.".i18n(key: "com.auth0.lock.error.unrecoverable.retry.title", comment: "Unrecoverable error retry title") + actionButton.title = "Retry request".i18n(key: "com.auth0.lock.error.unrecoverable.retry.button", comment: "Retry button title") } else { - actionLabel.text = "Please contact ".i18n(key: "com.auth0.lock.error.unrecoverable.support.title", comment: "Support label") - actionButton.title = "support.".i18n(key: "com.auth0.lock.error.unrecoverable.support.action", comment: "Support action") + messageLabel.text = "There was an unexpected error, please contact support.".i18n(key: "com.auth0.lock.error.unrecoverable.support.title", comment: "Unrecoverable error support title") + actionButton.title = "Contact support".i18n(key: "com.auth0.lock.error.unrecoverable.support.button", comment: "Support button title") + actionButton.isHidden = true } } diff --git a/LockTests/OptionsSpec.swift b/LockTests/OptionsSpec.swift index fb2826fee..2065883ba 100644 --- a/LockTests/OptionsSpec.swift +++ b/LockTests/OptionsSpec.swift @@ -57,6 +57,14 @@ class OptionsSpec: QuickSpec { expect(options.privacyPolicyURL.absoluteString) == "https://auth0.com/privacy" } + it("should have Auth0 support as nil") { + expect(options.supportURL).to(beNil()) + } + + it("should return Auth0 supportPage as nil") { + expect(options.supportPage).to(beNil()) + } + it("should have openid as scope") { expect(options.scope) == "openid" } @@ -223,6 +231,16 @@ class OptionsSpec: QuickSpec { options.privacyPolicy = "not a url" expect(options.privacyPolicyURL.absoluteString) == "https://auth0.com/privacy" } + + it("should set support site") { + options.supportPage = "https://auth0.com/docs" + expect(options.supportURL?.absoluteString) == "https://auth0.com/docs" + } + + it("should ignore invalid support site") { + options.supportPage = "not a url" + expect(options.supportURL?.absoluteString).to(beNil()) + } } } } diff --git a/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift b/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift index 8554891f5..529debddb 100644 --- a/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift +++ b/LockTests/Presenters/UnrecoverableErrorPresenterSpec.swift @@ -34,11 +34,13 @@ class UnrecoverableErrorPresenterSpec: QuickSpec { var navigator: MockNavigator! var error: UnrecoverableError! var view: UnrecoverableErrorView! + var options: OptionBuildable! beforeEach { + options = LockOptions() error = UnrecoverableError.connectionTimeout navigator = MockNavigator() - presenter = UnrecoverableErrorPresenter(error: error, navigator: navigator) + presenter = UnrecoverableErrorPresenter(error: error, navigator: navigator, options: options) view = presenter.view as? UnrecoverableErrorView } @@ -51,24 +53,41 @@ class UnrecoverableErrorPresenterSpec: QuickSpec { context("retry error") { beforeEach { - presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator) + presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator, options: options) view = presenter.view as? UnrecoverableErrorView } it("should have relevant retry button title") { - expect(view.secondaryButton?.title?.contains("retry")) == true + expect(view.secondaryButton?.title?.contains("Retry")) == true } } - context("support error") { + context("support error with no page (default)") { beforeEach { - presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator) + presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator, options: options) view = presenter.view as? UnrecoverableErrorView } - it("should have relevant support button title") { - expect(view.secondaryButton?.title?.contains("support")) == true + it("should not display support button") { + expect(view.secondaryButton?.isHidden) == true + } + } + + context("support error with support page provided") { + + beforeEach { + options.supportPage = "http://auth0.com/docs" + presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator, options: options) + view = presenter.view as? UnrecoverableErrorView + } + + it("should have a support button title") { + expect(view.secondaryButton?.title?.contains("Contact")) == true + } + + it("should have a visible support button") { + expect(view.secondaryButton?.isHidden) == false } } } @@ -78,7 +97,7 @@ class UnrecoverableErrorPresenterSpec: QuickSpec { context("retry error") { beforeEach { - presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator) + presenter = UnrecoverableErrorPresenter(error: .connectionTimeout, navigator: navigator, options: options) view = presenter.view as? UnrecoverableErrorView } @@ -91,7 +110,7 @@ class UnrecoverableErrorPresenterSpec: QuickSpec { context("support error") { beforeEach { - presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator) + presenter = UnrecoverableErrorPresenter(error: .invalidClientOrDomain, navigator: navigator, options: options) view = presenter.view as? UnrecoverableErrorView } From 20b00451ebdf76735809010d347396207b6d4da9 Mon Sep 17 00:00:00 2001 From: Martin Walsh Date: Mon, 24 Apr 2017 17:00:49 +0100 Subject: [PATCH 3/4] Update Unrecoverable Design & Copy with latest spec --- Lock/Base.lproj/Lock.strings | 22 ++++++++++++------- Lock/UnrecoverableErrorPresenter.swift | 1 + Lock/UnrecoverableErrorView.swift | 30 ++++++++++++++++++++------ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/Lock/Base.lproj/Lock.strings b/Lock/Base.lproj/Lock.strings index ca3d0dd62..ad929282e 100644 --- a/Lock/Base.lproj/Lock.strings +++ b/Lock/Base.lproj/Lock.strings @@ -68,6 +68,12 @@ "com.auth0.lock.error.passwordless.invalid_link" = "WE'RE SORRY, THERE WAS A PROBLEM WITH YOUR LINK. PLEASE REQUEST A NEW ONE."; // Passwordless sign ups disabled. "com.auth0.lock.error.passwordless.signup_disabled" = "NEW SIGN UPS ARE DISABLED FOR THIS ACCOUNT, PLEASE CONTACT YOUR ADMINISTRATOR."; +// Recoverable error message +"com.auth0.lock.error.recoverable.message" = "Please check your internet connection."; +// Recoverable error button +"com.auth0.lock.error.recoverable.retry.button" = "Retry"; +// Recoverable error title +"com.auth0.lock.error.recoverable.title" = "Can't load the login box"; // Generic sign up error "com.auth0.lock.error.signup.fallback" = "WE'RE SORRY, SOMETHING WENT WRONG WHEN ATTEMPTING TO SIGN UP."; // invalid_password @@ -80,22 +86,22 @@ "com.auth0.lock.error.signup.password_no_user_info_error" = "PASSWORD IS BASED ON USER INFORMATION."; // password_strength_error "com.auth0.lock.error.signup.password_strength_error" = "PASSWORD IS TOO WEAK."; +// Unrecoverable error button +"com.auth0.lock.error.unrecoverable.button" = "Contact support"; // Default error "com.auth0.lock.error.unrecoverable.default" = "Something went wrong.\nPlease contact technical support."; // Invalid client or domain "com.auth0.lock.error.unrecoverable.invalid_credentials" = "Your Auth0 credentials ClientId and/or Domain are invalid."; // Your options configuration failed with: %@{error} "com.auth0.lock.error.unrecoverable.invalid_options" = "Your options configuration failed with: %1$@"; +// Unrecoverable error message +"com.auth0.lock.error.unrecoverable.message" = "There was an unexpected error while resolving the login box configuration."; +// Unrecoverable error message +"com.auth0.lock.error.unrecoverable.message.no_action" = "There was an unexpected error while resolving the login box configuration, please contact support."; // No connections "com.auth0.lock.error.unrecoverable.no_connections" = "No authentication methods found for this client. please check your client setup."; -// Retry button title -"com.auth0.lock.error.unrecoverable.retry.button" = "Retry request"; -// Unrecoverable error retry title -"com.auth0.lock.error.unrecoverable.retry.title" = "There was a problem with the request."; -// Support button title -"com.auth0.lock.error.unrecoverable.support.button" = "Contact support"; -// Unrecoverable error support title -"com.auth0.lock.error.unrecoverable.support.title" = "There was an unexpected error, please contact support."; +// Unrecoverable error title +"com.auth0.lock.error.unrecoverable.title" = "Can't resolve your request"; // Forgot Password message "com.auth0.lock.forgot.message" = "Please enter your email and the new password. We will send you an email to confirm the password change."; // Forgot Password title diff --git a/Lock/UnrecoverableErrorPresenter.swift b/Lock/UnrecoverableErrorPresenter.swift index 8d83b9cdd..0721a70e8 100644 --- a/Lock/UnrecoverableErrorPresenter.swift +++ b/Lock/UnrecoverableErrorPresenter.swift @@ -46,6 +46,7 @@ class UnrecoverableErrorPresenter: Presentable { UIApplication.shared.openURL(supportURL) } view.secondaryButton?.isHidden = false + view.messageLabel?.text = "There was an unexpected error while resolving the login box configuration.".i18n(key: "com.auth0.lock.error.unrecoverable.message", comment: "Unrecoverable error message") } return view } diff --git a/Lock/UnrecoverableErrorView.swift b/Lock/UnrecoverableErrorView.swift index 1e3e14002..732b5af2d 100644 --- a/Lock/UnrecoverableErrorView.swift +++ b/Lock/UnrecoverableErrorView.swift @@ -25,17 +25,21 @@ import UIKit class UnrecoverableErrorView: UIView, View { weak var secondaryButton: SecondaryButton? + weak var messageLabel: UILabel? init(canRetry: Bool) { let center = UILayoutGuide() + let titleLabel = UILabel() let messageLabel = UILabel() let imageView = UIImageView() let actionButton = SecondaryButton() self.secondaryButton = actionButton + self.messageLabel = messageLabel super.init(frame: CGRect.zero) self.addSubview(imageView) + self.addSubview(titleLabel) self.addSubview(messageLabel) self.addSubview(actionButton) self.addLayoutGuide(center) @@ -49,28 +53,40 @@ class UnrecoverableErrorView: UIView, View { constraintEqual(anchor: imageView.centerYAnchor, toAnchor: center.centerYAnchor, constant: -90) imageView.translatesAutoresizingMaskIntoConstraints = false + constraintEqual(anchor: titleLabel.leftAnchor, toAnchor: self.leftAnchor, constant: 20) + constraintEqual(anchor: titleLabel.rightAnchor, toAnchor: self.rightAnchor, constant: -20) + constraintEqual(anchor: titleLabel.centerYAnchor, toAnchor: center.centerYAnchor, constant: -15) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + constraintEqual(anchor: messageLabel.leftAnchor, toAnchor: self.leftAnchor, constant: 20) constraintEqual(anchor: messageLabel.rightAnchor, toAnchor: self.rightAnchor, constant: -20) - constraintEqual(anchor: messageLabel.centerYAnchor, toAnchor: center.centerYAnchor) + constraintEqual(anchor: messageLabel.topAnchor, toAnchor: titleLabel.bottomAnchor, constant: 15) messageLabel.translatesAutoresizingMaskIntoConstraints = false constraintEqual(anchor: actionButton.centerXAnchor, toAnchor: center.centerXAnchor) - constraintEqual(anchor: actionButton.centerYAnchor, toAnchor: center.centerYAnchor, constant: 70) + constraintEqual(anchor: actionButton.topAnchor, toAnchor: messageLabel.bottomAnchor, constant: 10) actionButton.translatesAutoresizingMaskIntoConstraints = false imageView.image = LazyImage(name: "ic_connection_error", bundle: bundleForLock()).image(compatibleWithTraits: self.traitCollection) + titleLabel.textAlignment = .center + titleLabel.font = lightSystemFont(size: 22) + titleLabel.numberOfLines = 1 messageLabel.textAlignment = .center - messageLabel.font = lightSystemFont(size: 22) + messageLabel.font = regularSystemFont(size: 15) + messageLabel.textColor = UIColor(red: 0.408, green: 0.408, blue: 0.408, alpha: 1.00) messageLabel.numberOfLines = 3 + actionButton.button?.setTitleColor(UIColor(red:0.04, green:0.53, blue:0.69, alpha:1.0), for: .normal) actionButton.button?.titleLabel?.font = regularSystemFont(size: 16) if canRetry { - messageLabel.text = "There was a problem with the request.".i18n(key: "com.auth0.lock.error.unrecoverable.retry.title", comment: "Unrecoverable error retry title") - actionButton.title = "Retry request".i18n(key: "com.auth0.lock.error.unrecoverable.retry.button", comment: "Retry button title") + titleLabel.text = "Can't load the login box".i18n(key: "com.auth0.lock.error.recoverable.title", comment: "Recoverable error title") + messageLabel.text = "Please check your internet connection.".i18n(key: "com.auth0.lock.error.recoverable.message", comment: "Recoverable error message") + actionButton.title = "Retry".i18n(key: "com.auth0.lock.error.recoverable.retry.button", comment: "Recoverable error button") } else { - messageLabel.text = "There was an unexpected error, please contact support.".i18n(key: "com.auth0.lock.error.unrecoverable.support.title", comment: "Unrecoverable error support title") - actionButton.title = "Contact support".i18n(key: "com.auth0.lock.error.unrecoverable.support.button", comment: "Support button title") + titleLabel.text = "Can't resolve your request".i18n(key: "com.auth0.lock.error.unrecoverable.title", comment: "Unrecoverable error title") + messageLabel.text = "There was an unexpected error while resolving the login box configuration, please contact support.".i18n(key: "com.auth0.lock.error.unrecoverable.message.no_action", comment: "Unrecoverable error message") + actionButton.title = "Contact support".i18n(key: "com.auth0.lock.error.unrecoverable.button", comment: "Unrecoverable error button") actionButton.isHidden = true } } From 914db26cf9e3437628240e9e09d805ca5497505e Mon Sep 17 00:00:00 2001 From: Martin Walsh Date: Wed, 26 Apr 2017 17:11:17 +0100 Subject: [PATCH 4/4] Update i18n reference --- Lock/Base.lproj/Lock.strings | 4 ++-- Lock/UnrecoverableErrorView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lock/Base.lproj/Lock.strings b/Lock/Base.lproj/Lock.strings index ad929282e..29ec31317 100644 --- a/Lock/Base.lproj/Lock.strings +++ b/Lock/Base.lproj/Lock.strings @@ -68,10 +68,10 @@ "com.auth0.lock.error.passwordless.invalid_link" = "WE'RE SORRY, THERE WAS A PROBLEM WITH YOUR LINK. PLEASE REQUEST A NEW ONE."; // Passwordless sign ups disabled. "com.auth0.lock.error.passwordless.signup_disabled" = "NEW SIGN UPS ARE DISABLED FOR THIS ACCOUNT, PLEASE CONTACT YOUR ADMINISTRATOR."; +// Recoverable error button +"com.auth0.lock.error.recoverable.button" = "Retry"; // Recoverable error message "com.auth0.lock.error.recoverable.message" = "Please check your internet connection."; -// Recoverable error button -"com.auth0.lock.error.recoverable.retry.button" = "Retry"; // Recoverable error title "com.auth0.lock.error.recoverable.title" = "Can't load the login box"; // Generic sign up error diff --git a/Lock/UnrecoverableErrorView.swift b/Lock/UnrecoverableErrorView.swift index 732b5af2d..ceba97a17 100644 --- a/Lock/UnrecoverableErrorView.swift +++ b/Lock/UnrecoverableErrorView.swift @@ -82,7 +82,7 @@ class UnrecoverableErrorView: UIView, View { if canRetry { titleLabel.text = "Can't load the login box".i18n(key: "com.auth0.lock.error.recoverable.title", comment: "Recoverable error title") messageLabel.text = "Please check your internet connection.".i18n(key: "com.auth0.lock.error.recoverable.message", comment: "Recoverable error message") - actionButton.title = "Retry".i18n(key: "com.auth0.lock.error.recoverable.retry.button", comment: "Recoverable error button") + actionButton.title = "Retry".i18n(key: "com.auth0.lock.error.recoverable.button", comment: "Recoverable error button") } else { titleLabel.text = "Can't resolve your request".i18n(key: "com.auth0.lock.error.unrecoverable.title", comment: "Unrecoverable error title") messageLabel.text = "There was an unexpected error while resolving the login box configuration, please contact support.".i18n(key: "com.auth0.lock.error.unrecoverable.message.no_action", comment: "Unrecoverable error message")