import Foundation import UIKit import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils private enum CreatePasswordField { case password case passwordConfirmation case hint case email } private final class CreatePasswordControllerArguments { let context: AccountContext let updateFieldText: (CreatePasswordField, String) -> Void let selectNextInputItem: (CreatePasswordEntryTag) -> Void let save: () -> Void let cancelEmailConfirmation: () -> Void init(context: AccountContext, updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { self.context = context self.updateFieldText = updateFieldText self.selectNextInputItem = selectNextInputItem self.save = save self.cancelEmailConfirmation = cancelEmailConfirmation } } private enum CreatePasswordSection: Int32 { case password case hint case email case emailCancel } private enum CreatePasswordEntryTag: ItemListItemTag { case password case passwordConfirmation case hint case email func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? CreatePasswordEntryTag { return self == other } else { return false } } } private enum CreatePasswordEntry: ItemListNodeEntry, Equatable { case passwordHeader(PresentationTheme, String) case password(PresentationTheme, PresentationStrings, String, String) case passwordConfirmation(PresentationTheme, PresentationStrings, String, String) case passwordInfo(PresentationTheme, String) case hintHeader(PresentationTheme, String) case hint(PresentationTheme, PresentationStrings, String, String, Bool) case hintInfo(PresentationTheme, String) case emailHeader(PresentationTheme, String) case email(PresentationTheme, PresentationStrings, String, String) case emailInfo(PresentationTheme, String) case emailConfirmation(PresentationTheme, String) case emailCancel(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { case .passwordHeader, .password, .passwordConfirmation, .passwordInfo: return CreatePasswordSection.password.rawValue case .hintHeader, .hint, .hintInfo: return CreatePasswordSection.hint.rawValue case .emailHeader, .email, .emailInfo, .emailConfirmation: return CreatePasswordSection.email.rawValue case .emailCancel: return CreatePasswordSection.emailCancel.rawValue } } var stableId: Int32 { switch self { case .passwordHeader: return 0 case .password: return 1 case .passwordConfirmation: return 2 case .passwordInfo: return 3 case .hintHeader: return 4 case .hint: return 5 case .hintInfo: return 6 case .emailHeader: return 7 case .email: return 8 case .emailInfo: return 9 case .emailConfirmation: return 10 case .emailCancel: return 11 } } static func <(lhs: CreatePasswordEntry, rhs: CreatePasswordEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreatePasswordControllerArguments switch self { case let .passwordHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .password(_, _, text, value): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.password, updatedText) }, action: { arguments.selectNextInputItem(CreatePasswordEntryTag.password) }) case let .passwordConfirmation(_, _, text, value): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.passwordConfirmation, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.passwordConfirmation, updatedText) }, action: { arguments.selectNextInputItem(CreatePasswordEntryTag.passwordConfirmation) }) case let .passwordInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .hintHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .hint(_, _, text, value, last): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .regular(capitalization: true, autocorrection: false), returnKeyType: last ? .done : .next, spacing: 0.0, tag: CreatePasswordEntryTag.hint, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.hint, updatedText) }, action: { if last { arguments.save() } else { arguments.selectNextInputItem(CreatePasswordEntryTag.hint) } }) case let .hintInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .email(_, _, text, value): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .email, returnKeyType: .done, spacing: 0.0, tag: CreatePasswordEntryTag.email, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.email, updatedText) }, action: { arguments.save() }) case let .emailInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailConfirmation(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailCancel(_, text, enabled): return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.cancelEmailConfirmation() }) } } } private struct CreatePasswordControllerState: Equatable { var state: CreatePasswordState var passwordText: String = "" var passwordConfirmationText: String = "" var hintText: String = "" var emailText: String = "" var saving: Bool = false init(state: CreatePasswordState) { self.state = state } } private func createPasswordControllerEntries(presentationData: PresentationData, context: CreatePasswordContext, state: CreatePasswordControllerState) -> [CreatePasswordEntry] { var entries: [CreatePasswordEntry] = [] switch state.state { case let .setup(currentPassword): entries.append(.passwordHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordSection)) entries.append(.password(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_PasswordPlaceholder, state.passwordText)) entries.append(.passwordConfirmation(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_PasswordConfirmationPlaceholder, state.passwordConfirmationText)) if case .paymentInfo = context { entries.append(.passwordInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordHelp)) } let showEmail = currentPassword == nil entries.append(.hintHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintSection)) entries.append(.hint(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_HintPlaceholder, state.hintText, !showEmail)) entries.append(.hintInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintHelp)) if showEmail { entries.append(.emailHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailSection)) entries.append(.email(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_EmailPlaceholder, state.emailText)) entries.append(.emailInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailHelp)) } case let .pendingVerification(emailPattern): entries.append(.emailConfirmation(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\(emailPattern)")) entries.append(.emailCancel(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationAbort, !state.saving)) } return entries } enum CreatePasswordContext { case account case secureId case paymentInfo } enum CreatePasswordState: Equatable { case setup(currentPassword: String?) case pendingVerification(emailPattern: String) } func createPasswordController(context: AccountContext, createPasswordContext: CreatePasswordContext, state: CreatePasswordState, completion: @escaping (String, String, Bool) -> Void, updatePasswordEmailConfirmation: @escaping ((String, String)?) -> Void, processPasswordEmailConfirmation: Bool = true) -> ViewController { let statePromise = ValuePromise(CreatePasswordControllerState(state: state), ignoreRepeated: true) let stateValue = Atomic(value: CreatePasswordControllerState(state: state)) let updateState: ((CreatePasswordControllerState) -> CreatePasswordControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? let actionsDisposable = DisposableSet() let saveDisposable = MetaDisposable() actionsDisposable.add(saveDisposable) var initialFocusImpl: (() -> Void)? var selectNextInputItemImpl: ((CreatePasswordEntryTag) -> Void)? let saveImpl = { var state: CreatePasswordControllerState? updateState { s in state = s return s } if let state = state { let presentationData = context.sharedContext.currentPresentationData.with { $0 } if state.passwordText.isEmpty { } else if state.passwordText != state.passwordConfirmationText { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } else { let saveImpl: () -> Void = { var currentPassword: String? var email: String? updateState { state in var state = state if case let .setup(password) = state.state { currentPassword = password if password != nil { email = nil } else { email = state.emailText } } state.saving = true return state } saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .password(password: state.passwordText, hint: state.hintText, email: email)) |> deliverOnMainQueue).start(next: { update in switch update { case .none: break case let .password(password, pendingEmail): if let pendingEmail = pendingEmail, let email = email { if processPasswordEmailConfirmation { updateState { state in var state = state state.saving = false state.state = .pendingVerification(emailPattern: pendingEmail.pattern) return state } } updatePasswordEmailConfirmation((email, pendingEmail.pattern)) } else { completion(password, state.hintText, !state.emailText.isEmpty) } } }, error: { _ in presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) })) } var emailAlert = false switch state.state { case let .setup(currentPassword): if currentPassword != nil { emailAlert = false } else { emailAlert = state.emailText.isEmpty } case .pendingVerification: break } if emailAlert { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { saveImpl() })]), nil) } else { saveImpl() } } } } let arguments = CreatePasswordControllerArguments(context: context, updateFieldText: { field, updatedText in updateState { state in var state = state switch field { case .password: state.passwordText = updatedText case .passwordConfirmation: state.passwordConfirmationText = updatedText case .hint: state.hintText = updatedText case .email: state.emailText = updatedText } return state } }, selectNextInputItem: { tag in selectNextInputItemImpl?(tag) }, save: { saveImpl() }, cancelEmailConfirmation: { var currentPassword: String? updateState { state in var state = state switch state.state { case let .setup(password): currentPassword = password case .pendingVerification: currentPassword = nil } state.saving = true return state } saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in updateState { state in var state = state state.saving = false state.state = .setup(currentPassword: nil) return state } updatePasswordEmailConfirmation(nil) }, error: { _ in updateState { state in var state = state state.saving = false return state } })) }) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.saving { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { switch state.state { case .setup: rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.passwordText.isEmpty, action: { saveImpl() }) case .pendingVerification: break } } let title: String switch state.state { case let .setup(currentPassword): if currentPassword != nil { title = presentationData.strings.TwoStepAuth_ChangePassword } else { title = presentationData.strings.FastTwoStepSetup_Title } case .pendingVerification: title = presentationData.strings.FastTwoStepSetup_Title } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPasswordControllerEntries(presentationData: presentationData, context: createPasswordContext, state: state), style: .blocks, focusItemTag: CreatePasswordEntryTag.password, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) dismissImpl = { [weak controller] in controller?.view.endEditing(true) controller?.dismiss() } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } initialFocusImpl = { [weak controller] in guard let controller = controller, controller.didAppearOnce else { return } var resultItemNode: ItemListSingleLineInputItemNode? let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: CreatePasswordEntryTag.password) { resultItemNode = itemNode return true } return false }) if let resultItemNode = resultItemNode { resultItemNode.focus() } } selectNextInputItemImpl = { [weak controller] currentTag in guard let controller = controller else { return } var resultItemNode: ItemListSingleLineInputItemNode? var focusOnNext = false let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag { if focusOnNext && resultItemNode == nil { resultItemNode = itemNode return true } else if currentTag.isEqual(to: tag) { focusOnNext = true } } return false }) if let resultItemNode = resultItemNode { resultItemNode.focus() } } controller.didAppear = { firstTime in if !firstTime { return } initialFocusImpl?() } return controller }