Antidote/Antidote/LoginCoordinator.swift

314 lines
12 KiB
Swift

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol LoginCoordinatorDelegate: class {
func loginCoordinatorDidLogin(_ coordinator: LoginCoordinator, manager: OCTManager, password: String)
}
private enum UserInfoKey: String {
case ImportURL
case LoginProfile
case LoginConfigurationClosure
case LoginErrorClosure
}
class LoginCoordinator {
weak var delegate: LoginCoordinatorDelegate?
fileprivate let window: UIWindow
fileprivate let theme: Theme
fileprivate let navigationController: UINavigationController
fileprivate var createAccountCoordinator: LoginCreateAccountCoordinator?
init(theme: Theme, window: UIWindow) {
self.window = window
self.theme = theme
switch InterfaceIdiom.current() {
case .iPhone:
self.navigationController = PortraitNavigationController()
case .iPad:
self.navigationController = UINavigationController()
}
navigationController.navigationBar.tintColor = theme.colorForType(.LoginButtonBackground)
navigationController.navigationBar.titleTextAttributes = [
NSAttributedStringKey.foregroundColor: theme.colorForType(.LoginButtonText)
]
}
}
// MARK: TopCoordinatorProtocol
extension LoginCoordinator: TopCoordinatorProtocol {
func startWithOptions(_ options: CoordinatorOptions?) {
let profileNames = ProfileManager().allProfileNames
let controller: UIViewController = (profileNames.count > 0) ? createFormController() : createChoiceController()
navigationController.pushViewController(controller, animated: false)
window.rootViewController = navigationController
}
func handleLocalNotification(_ notification: UILocalNotification) {
// nop
}
func handleInboxURL(_ url: URL) {
guard url.isToxURL() else {
return
}
let fileName = url.lastPathComponent
let style: UIAlertControllerStyle
switch InterfaceIdiom.current() {
case .iPhone:
style = .actionSheet
case .iPad:
style = .alert
}
let alert = UIAlertController(title: nil, message: fileName, preferredStyle: style)
alert.addAction(UIAlertAction(title: String(localized: "create_profile"), style: .default) { [unowned self] _ -> Void in
self.importToxProfileFromURL(url)
})
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .cancel, handler: nil))
navigationController.present(alert, animated: true, completion: nil)
}
}
extension LoginCoordinator: LoginFormControllerDelegate {
func loginFormControllerLogin(_ controller: LoginFormController, profileName: String, password: String?) {
loginWithProfile(profileName, password: password, errorClosure: { error in
handleErrorWithType(.createOCTManager, error: error)
})
}
func loginFormControllerCreateAccount(_ controller: LoginFormController) {
showCreateAccountController()
}
func loginFormControllerImportProfile(_ controller: LoginFormController) {
showImportProfileController()
}
func loginFormController(_ controller: LoginFormController, isProfileEncrypted profile: String) -> Bool {
return isProfileEncrypted(profile)
}
}
extension LoginCoordinator: LoginChoiceControllerDelegate {
func loginChoiceControllerCreateAccount(_ controller: LoginChoiceController) {
showCreateAccountController()
}
func loginChoiceControllerImportProfile(_ controller: LoginChoiceController) {
showImportProfileController()
}
}
extension LoginCoordinator: LoginCreateAccountCoordinatorDelegate {
func loginCreateAccountCoordinator(_ coordinator: LoginCreateAccountCoordinator,
didCreateAccountWithProfileName profileName: String,
username: String,
password: String) {
createProfileWithProfileName(profileName, username: username, copyFromURL: nil, password: password)
}
func loginCreateAccountCoordinator(_ coordinator: LoginCreateAccountCoordinator,
didImportProfileWithProfileName profileName: String) {
guard let url = coordinator.userInfo[UserInfoKey.ImportURL.rawValue] as? URL else {
fatalError("URL should be non-nil when importing profile")
}
createProfileWithProfileName(profileName, username: nil, copyFromURL: url, password: nil)
}
func loginCreateAccountCoordinator(_ coordinator: LoginCreateAccountCoordinator, didCreatePassword password: String) {
guard let profile = coordinator.userInfo[UserInfoKey.LoginProfile.rawValue] as? String else {
fatalError("Profile should be non-nil on login")
}
let configurationClosure = coordinator.userInfo[UserInfoKey.LoginConfigurationClosure.rawValue] as? ((_ manager: OCTManager) -> Void)
let errorClosure = coordinator.userInfo[UserInfoKey.LoginErrorClosure.rawValue] as? ((NSError) -> Void)
loginWithProfile(profile, password: password, configurationClosure: configurationClosure, errorClosure: errorClosure)
}
}
private extension LoginCoordinator {
func createFormController() -> LoginFormController {
let profileNames = ProfileManager().allProfileNames
var selectedIndex = 0
if let activeProfile = UserDefaultsManager().lastActiveProfile {
selectedIndex = profileNames.index(of: activeProfile) ?? 0
}
let controller = LoginFormController(theme: theme, profileNames: profileNames, selectedIndex: selectedIndex)
controller.delegate = self
return controller
}
func createChoiceController() -> LoginChoiceController {
let controller = LoginChoiceController(theme: theme)
controller.delegate = self
return controller
}
func showCreateAccountController() {
let coordinator = LoginCreateAccountCoordinator(theme: theme,
navigationController: navigationController,
type: .createAccountAndPassword)
coordinator.delegate = self
coordinator.startWithOptions(nil)
createAccountCoordinator = coordinator
}
func showImportProfileController() {
let controller = TextViewController(
resourceName: "import-profile",
backgroundColor: theme.colorForType(.LoginBackground),
titleColor: theme.colorForType(.NormalText),
textColor: theme.colorForType(.NormalText))
navigationController.pushViewController(controller, animated: true)
}
/**
* @param profile The name of profile.
* @param password Password to decrypt profile.
* @param configurationClosure Closure called after login to configure profile.
* @param errorClosure Closure called if any error occured during login.
*/
func loginWithProfile(
_ profile: String,
password: String?,
configurationClosure: ((_ manager: OCTManager) -> Void)? = nil,
errorClosure: ((NSError) -> Void)? = nil)
{
guard let password = password else {
if isProfileEncrypted(profile) {
// Profile is encrypted, password is required. No error is needed, password placeholder
// should be quite obvious.
// However we should show error message for accessibility users.
if UIAccessibilityIsVoiceOverRunning() {
handleErrorWithType(.passwordIsEmpty)
}
return
}
let coordinator = LoginCreateAccountCoordinator(theme: theme,
navigationController: navigationController,
type: .createPassword)
coordinator.delegate = self
coordinator.userInfo[UserInfoKey.LoginProfile.rawValue] = profile
coordinator.userInfo[UserInfoKey.LoginConfigurationClosure.rawValue] = configurationClosure
coordinator.userInfo[UserInfoKey.LoginErrorClosure.rawValue] = errorClosure
coordinator.startWithOptions(nil)
createAccountCoordinator = coordinator
return
}
let path = ProfileManager().pathForProfileWithName(profile)
let configuration = OCTManagerConfiguration.configurationWithBaseDirectory(path)!
let hud = JGProgressHUD(style: .dark)
hud.show(in: self.navigationController.view)
ToxFactory.createToxWithConfiguration(configuration, encryptPassword: password, successBlock: { [weak self] manager -> Void in
hud.dismiss()
configurationClosure?(manager)
let userDefaults = UserDefaultsManager()
userDefaults.lastActiveProfile = profile
self?.delegate?.loginCoordinatorDidLogin(self!, manager: manager, password: password)
}, failureBlock: { error -> Void in
hud.dismiss()
errorClosure?(error as NSError)
})
}
func importToxProfileFromURL(_ url: URL) {
let coordinator = LoginCreateAccountCoordinator(theme: theme,
navigationController: navigationController,
type: .importProfile)
coordinator.userInfo[UserInfoKey.ImportURL.rawValue] = url
coordinator.delegate = self
coordinator.startWithOptions(nil)
createAccountCoordinator = coordinator
}
func createProfileWithProfileName(_ profileName: String, username: String?, copyFromURL: URL?, password: String?) {
if profileName.isEmpty {
UIAlertController.showWithTitle("", message: String(localized: "login_enter_username_and_profile"), retryBlock: nil)
return
}
let profileManager = ProfileManager()
do {
try profileManager.createProfileWithName(profileName, copyFromURL: copyFromURL)
}
catch let error as NSError {
UIAlertController.showErrorWithMessage(String(localized: error.localizedDescription), retryBlock: nil)
return
}
if isProfileEncrypted(profileName) && password == nil {
// Cannot login without password, just opening login screen.
UserDefaultsManager().lastActiveProfile = profileName
let controller = self.createFormController()
let root = self.navigationController.viewControllers[0]
self.navigationController.setViewControllers([root, controller], animated: true)
return
}
loginWithProfile(profileName, password: password, configurationClosure: {
if let name = username {
_ = try? $0.user.setUserName(name)
_ = try? $0.user.setUserStatusMessage("Toxing on Antidote")
}
}, errorClosure: { error in
handleErrorWithType(.createOCTManager, error: error)
_ = try? profileManager.deleteProfileWithName(profileName)
})
}
func isProfileEncrypted(_ profile: String) -> Bool {
let profilePath = ProfileManager().pathForProfileWithName(profile)
let configuration = OCTManagerConfiguration.configurationWithBaseDirectory(profilePath)!
let dataPath = configuration.fileStorage.pathForToxSaveFile
guard FileManager.default.fileExists(atPath: dataPath) else {
return false
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)) else {
return false
}
return OCTToxEncryptSave.isDataEncrypted(data)
}
}