import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SyncCore import SwiftSignalKit #if BUCK import MtProtoKit #else import MtProtoKitDynamic #endif import MessageUI import CoreTelephony import TelegramPresentationData import TextFormat import AccountContext import CountrySelectionUI import SettingsUI import PhoneNumberFormat private enum InnerState: Equatable { case state(UnauthorizedAccountStateContents) case authorized } public final class AuthorizationSequenceController: NavigationController, MFMailComposeViewControllerDelegate { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) } private let sharedContext: SharedAccountContext private var account: UnauthorizedAccount private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]) private let apiId: Int32 private let apiHash: String public var presentationData: PresentationData private let openUrl: (String) -> Void private let authorizationCompleted: () -> Void private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() private var didPlayPresentationAnimation = false private let _ready = Promise() override public var ready: Promise { return self._ready } private var didSetReady = false public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), presentationData: PresentationData, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { self.sharedContext = sharedContext self.account = account self.otherAccountPhoneNumbers = otherAccountPhoneNumbers self.apiId = apiId self.apiHash = apiHash self.presentationData = presentationData self.openUrl = openUrl self.authorizationCompleted = authorizationCompleted let navigationStatusBar: NavigationStatusBarStyle switch presentationData.theme.rootController.statusBarStyle { case .black: navigationStatusBar = .black case .white: navigationStatusBar = .white } super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), emptyAreaColor: .black)) self.stateDisposable = (account.postbox.stateView() |> map { view -> InnerState in if let _ = view.state as? AuthorizedAccountState { return .authorized } else if let state = view.state as? UnauthorizedAccountState { return .state(state.contents) } else { return .state(.empty) } } |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] state in self?.updateState(state: state) }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.stateDisposable?.dispose() self.actionDisposable.dispose() } override public func loadView() { super.loadView() self.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } private func splashController() -> AuthorizationSequenceSplashController { var currentController: AuthorizationSequenceSplashController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceSplashController { currentController = c break } } let controller: AuthorizationSequenceSplashController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequenceSplashController(accountManager: self.sharedContext.accountManager, postbox: self.account.postbox, network: self.account.network, theme: self.presentationData.theme) controller.nextPressed = { [weak self] strings in if let strongSelf = self { if let strings = strings { strongSelf.presentationData = strongSelf.presentationData.withStrings(strings) } let masterDatacenterId = strongSelf.account.masterDatacenterId let isTestingEnvironment = strongSelf.account.testingEnvironment let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: isTestingEnvironment, masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() } } } return controller } private func phoneEntryController(countryCode: Int32, number: String) -> AuthorizationSequencePhoneEntryController { var currentController: AuthorizationSequencePhoneEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequencePhoneEntryController { currentController = c break } } let controller: AuthorizationSequencePhoneEntryController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequencePhoneEntryController(sharedContext: self.sharedContext, account: self.account, isTestingEnvironment: self.account.testingEnvironment, otherAccountPhoneNumbers: self.otherAccountPhoneNumbers, network: self.account.network, presentationData: self.presentationData, openUrl: { [weak self] url in self?.openUrl(url) }, back: { [weak self] in guard let strongSelf = self else { return } if !strongSelf.otherAccountPhoneNumbers.1.isEmpty { let _ = (strongSelf.sharedContext.accountManager.transaction { transaction -> Void in transaction.removeAuth() }).start() } else { let _ = strongSelf.account.postbox.transaction({ transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .empty)) }).start() } }) controller.accountUpdated = { [weak self] updatedAccount in guard let strongSelf = self else { return } strongSelf.account = updatedAccount } controller.loginWithNumber = { [weak self, weak controller] number, syncContacts in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((sendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, phoneNumber: number, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, syncContacts: syncContacts) |> deliverOnMainQueue).start(next: { [weak self] account in if let strongSelf = self { controller?.inProgress = false strongSelf.account = account } }, error: { error in if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String var actions: [TextAlertAction] = [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ] switch error { case .limitExceeded: text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidPhoneNumber: text = strongSelf.presentationData.strings.Login_InvalidPhoneError actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } let formattedNumber = formatPhoneNumber(number) let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" let systemVersion = UIDevice.current.systemVersion let locale = Locale.current.identifier let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) })) case .phoneLimitExceeded: text = strongSelf.presentationData.strings.Login_PhoneFloodError case .phoneBanned: text = strongSelf.presentationData.strings.Login_PhoneBannedError actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } let formattedNumber = formatPhoneNumber(number) let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" let systemVersion = UIDevice.current.systemVersion let locale = Locale.current.identifier let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) })) case let .generic(info): text = strongSelf.presentationData.strings.Login_UnknownError actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } let formattedNumber = formatPhoneNumber(number) let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" let systemVersion = UIDevice.current.systemVersion let locale = Locale.current.identifier let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" let errorString: String if let (code, description) = info { errorString = "\(code): \(description)" } else { errorString = "unknown" } strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).0, from: controller) })) case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } controller.present(proxySettingsController(accountManager: strongSelf.sharedContext.accountManager, postbox: strongSelf.account.postbox, network: strongSelf.account.network, mode: .modal, presentationData: strongSelf.sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: strongSelf.sharedContext.presentationData), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root)) } })) } } } controller.updateData(countryCode: countryCode, countryName: nil, number: number) return controller } private func codeEntryController(number: String, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) -> AuthorizationSequenceCodeEntryController { var currentController: AuthorizationSequenceCodeEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceCodeEntryController { if c.data?.1 == type { currentController = c } break } } let controller: AuthorizationSequenceCodeEntryController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequenceCodeEntryController(presentationData: self.presentationData, openUrl: { [weak self] url in self?.openUrl(url) }, back: { [weak self] in guard let strongSelf = self else { return } let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() }) controller.loginWithCode = { [weak self, weak controller] code in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((authorizeWithCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, code: code, termsOfService: termsOfService?.0) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return } controller?.inProgress = false switch result { case let .signUp(data): if let (termsOfService, explicit) = termsOfService, explicit { var presentAlertAgainImpl: (() -> Void)? let presentAlertImpl: () -> Void = { guard let strongSelf = self else { return } var dismissImpl: (() -> Void)? let alertTheme = AlertControllerTheme(presentationData: strongSelf.presentationData) let attributedText = stringWithAppliedEntities(termsOfService.text, entities: termsOfService.entities, baseColor: alertTheme.primaryColor, linkColor: alertTheme.accentColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.regular(13.0), blockQuoteFont: Font.regular(13.0)) let contentNode = TextAlertContentNode(theme: alertTheme, title: NSAttributedString(string: strongSelf.presentationData.strings.Login_TermsOfServiceHeader, font: Font.medium(17.0), textColor: alertTheme.primaryColor, paragraphAlignment: .center), text: attributedText, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceAgree, action: { dismissImpl?() guard let strongSelf = self else { return } let _ = beginSignUp(account: strongSelf.account, data: data).start() }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, action: { dismissImpl?() guard let strongSelf = self else { return } strongSelf.currentWindow?.present(standardTextAlertController(theme: alertTheme, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, text: strongSelf.presentationData.strings.Login_TermsOfServiceSignupDecline, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { presentAlertAgainImpl?() }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, action: { guard let strongSelf = self else { return } let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() })]), on: .root, blockInteraction: false, completion: {}) }) ], actionLayout: .vertical) contentNode.textAttributeAction = (NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), { value in if let value = value as? String { strongSelf.openUrl(value) } }) let controller = AlertController(theme: alertTheme, contentNode: contentNode) dismissImpl = { [weak controller] in controller?.dismissAnimated() } strongSelf.view.endEditing(true) strongSelf.currentWindow?.present(controller, on: .root, blockInteraction: false, completion: {}) } presentAlertAgainImpl = { presentAlertImpl() } presentAlertImpl() } else { let _ = beginSignUp(account: strongSelf.account, data: data).start() } case .loggedIn: break } }, error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidCode: text = strongSelf.presentationData.strings.Login_InvalidCodeError case .generic: text = strongSelf.presentationData.strings.Login_UnknownError case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) } } } controller.requestNextOption = { [weak self, weak controller] in if let strongSelf = self { if nextType == nil { if MFMailComposeViewController.canSendMail(), let controller = controller { let formattedNumber = formatPhoneNumber(number) strongSelf.presentEmailComposeController(address: "sms@stel.com", subject: strongSelf.presentationData.strings.Login_EmailCodeSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_EmailCodeBody(formattedNumber).0, from: controller) } else { controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } else { controller?.inProgress = true strongSelf.actionDisposable.set((resendAuthorizationCode(account: strongSelf.account) |> deliverOnMainQueue).start(next: { result in controller?.inProgress = false }, error: { error in if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidPhoneNumber: text = strongSelf.presentationData.strings.Login_InvalidPhoneError case .phoneLimitExceeded: text = strongSelf.presentationData.strings.Login_PhoneFloodError case .phoneBanned: text = strongSelf.presentationData.strings.Login_PhoneBannedError case .generic: text = strongSelf.presentationData.strings.Login_UnknownError case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } } } controller.reset = { [weak self] in if let strongSelf = self { let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() } } controller.updateData(number: formatPhoneNumber(number), codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService) return controller } private func passwordEntryController(hint: String, suggestReset: Bool, syncContacts: Bool) -> AuthorizationSequencePasswordEntryController { var currentController: AuthorizationSequencePasswordEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequencePasswordEntryController { currentController = c break } } let controller: AuthorizationSequencePasswordEntryController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequencePasswordEntryController(presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() }) controller.loginWithPassword = { [weak self, weak controller] password in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((authorizeWithPassword(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, password: password, syncContacts: syncContacts) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: text = strongSelf.presentationData.strings.LoginPassword_FloodError case .invalidPassword: text = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError case .generic: text = strongSelf.presentationData.strings.Login_UnknownError } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) controller.passwordIsInvalid() } } })) } } } controller.forgot = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((requestPasswordRecovery(account: strongSelf.account) |> deliverOnMainQueue).start(next: { option in if let strongSelf = self, let strongController = controller { strongController.inProgress = false switch option { case let .email(pattern): let _ = (strongSelf.account.postbox.transaction { transaction -> Void in if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code, _, syncContacts) = state.contents { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))) } }).start() case .none: strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongController.didForgotWithNoRecovery = true } } }, error: { error in if let strongController = controller { strongController.inProgress = false } })) } } controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) |> deliverOnMainQueue).start(next: { if let strongController = controller { strongController.inProgress = false } }, error: { error in if let strongSelf = self, let strongController = controller { strongController.inProgress = false let text: String switch error { case .generic: text = strongSelf.presentationData.strings.Login_UnknownError case .limitExceeded: text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } })]), in: .window(.root)) } } controller.updateData(hint: hint, suggestReset: suggestReset) return controller } private func passwordRecoveryController(emailPattern: String, syncContacts: Bool) -> AuthorizationSequencePasswordRecoveryController { var currentController: AuthorizationSequencePasswordRecoveryController? for c in self.viewControllers { if let c = c as? AuthorizationSequencePasswordRecoveryController { currentController = c break } } let controller: AuthorizationSequencePasswordRecoveryController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequencePasswordRecoveryController(strings: self.presentationData.strings, theme: self.presentationData.theme, back: { [weak self] in guard let strongSelf = self else { return } let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() }) controller.recoverWithCode = { [weak self, weak controller] code in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((performPasswordRecovery(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, code: code, syncContacts: syncContacts) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: text = strongSelf.presentationData.strings.LoginPassword_FloodError case .invalidCode: text = strongSelf.presentationData.strings.Login_InvalidCodeError case .expired: text = strongSelf.presentationData.strings.Login_CodeExpiredError } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) } } controller.noAccess = { [weak self, weak controller] in if let strongSelf = self, let controller = controller { controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _, syncContacts) = state.contents { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))) } }).start() } } } controller.updateData(emailPattern: emailPattern) return controller } private func awaitingAccountResetController(protectedUntil: Int32, number: String?) -> AuthorizationSequenceAwaitingAccountResetController { var currentController: AuthorizationSequenceAwaitingAccountResetController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceAwaitingAccountResetController { currentController = c break } } let controller: AuthorizationSequenceAwaitingAccountResetController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequenceAwaitingAccountResetController(strings: self.presentationData.strings, theme: self.presentationData.theme, back: { [weak self] in guard let strongSelf = self else { return } let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() }) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) |> deliverOnMainQueue).start(next: { if let strongController = controller { strongController.inProgress = false } }, error: { error in if let strongSelf = self, let strongController = controller { strongController.inProgress = false let text: String switch error { case .generic: text = strongSelf.presentationData.strings.Login_UnknownError case .limitExceeded: text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } })]), in: .window(.root)) } } controller.logout = { [weak self] in if let strongSelf = self { let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() } } } controller.updateData(protectedUntil: protectedUntil, number: number ?? "") return controller } private func signUpController(firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?, displayCancel: Bool) -> AuthorizationSequenceSignUpController { var currentController: AuthorizationSequenceSignUpController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceSignUpController { currentController = c break } } let controller: AuthorizationSequenceSignUpController if let currentController = currentController { controller = currentController } else { controller = AuthorizationSequenceSignUpController(presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } let countryCode = defaultCountryCode() let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) }).start() }, displayCancel: displayCancel) controller.signUpWithName = { [weak self, weak controller] firstName, lastName, avatarData in if let strongSelf = self { controller?.inProgress = true strongSelf.actionDisposable.set((signUpWithName(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, firstName: firstName, lastName: lastName, avatarData: avatarData) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false let text: String switch error { case .limitExceeded: text = strongSelf.presentationData.strings.Login_CodeFloodError case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpiredError case .invalidFirstName: text = strongSelf.presentationData.strings.Login_InvalidFirstNameError case .invalidLastName: text = strongSelf.presentationData.strings.Login_InvalidLastNameError case .generic: text = strongSelf.presentationData.strings.Login_UnknownError } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) } } } controller.updateData(firstName: firstName, lastName: lastName, termsOfService: termsOfService) return controller } private func updateState(state: InnerState) { switch state { case .authorized: self.authorizationCompleted() case let .state(state): switch state { case .empty: if let _ = self.viewControllers.last as? AuthorizationSequenceSplashController { } else { var controllers: [ViewController] = [] if self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } else { controllers.append(self.phoneEntryController(countryCode: defaultCountryCode(), number: "")) } self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } case let .phoneEntry(countryCode, number): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.phoneEntryController(countryCode: countryCode, number: number)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .confirmationCodeEntry(number, type, _, timeout, nextType, _): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.phoneEntryController(countryCode: defaultCountryCode(), number: "")) controllers.append(self.codeEntryController(number: number, type: type, nextType: nextType, timeout: timeout, termsOfService: nil)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .passwordEntry(hint, _, _, suggestReset, syncContacts): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.passwordEntryController(hint: hint, suggestReset: suggestReset, syncContacts: syncContacts)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .passwordRecovery(_, _, _, emailPattern, syncContacts): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.passwordRecoveryController(emailPattern: emailPattern, syncContacts: syncContacts)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .awaitingAccountReset(protectedUntil, number, _): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.awaitingAccountResetController(protectedUntil: protectedUntil, number: number)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .signUp(_, _, firstName, lastName, termsOfService, _): var controllers: [ViewController] = [] var displayCancel = false if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } else { displayCancel = true } controllers.append(self.signUpController(firstName: firstName, lastName: lastName, termsOfService: termsOfService, displayCancel: displayCancel)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } } } override public func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { let wasEmpty = self.viewControllers.isEmpty super.setViewControllers(viewControllers, animated: animated) if wasEmpty { self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } if !self.didSetReady { self.didSetReady = true self._ready.set(.single(true)) } } public func applyConfirmationCode(_ code: Int) { if let controller = self.viewControllers.last as? AuthorizationSequenceCodeEntryController { controller.applyConfirmationCode(code) } } private func presentEmailComposeController(address: String, subject: String, body: String, from controller: ViewController) { if MFMailComposeViewController.canSendMail() { let composeController = MFMailComposeViewController() composeController.setToRecipients([address]) composeController.setSubject(subject) composeController.setMessageBody(body, isHTML: false) composeController.mailComposeDelegate = self controller.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) } else { controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true, completion: nil) } private func animateIn() { self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } private func animateOut(completion: (() -> Void)? = nil) { self.view.layer.animatePosition(from: self.view.layer.position, to: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in completion?() }) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.didPlayPresentationAnimation { self.didPlayPresentationAnimation = true self.animateIn() } } public func dismiss() { self.animateOut(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) } } private func defaultCountryCode() -> Int32 { var countryId: String? = nil let networkInfo = CTTelephonyNetworkInfo() if let carrier = networkInfo.subscriberCellularProvider { countryId = carrier.isoCountryCode } if countryId == nil { countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String } var countryCode: Int32 = 1 if let countryId = countryId { let normalizedId = countryId.uppercased() for (code, idAndName) in countryCodeToIdAndName { if idAndName.0 == normalizedId { countryCode = Int32(code) break } } } return countryCode }