314 lines
12 KiB
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)
|
|
}
|
|
}
|