Swiftgram/TelegramUI/TwoStepVerificationUnlockController.swift
Peter Iakovlev 47cd4b0d76 no message
2018-01-31 20:40:55 +04:00

543 lines
30 KiB
Swift

import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class TwoStepVerificationUnlockSettingsControllerArguments {
let updatePasswordText: (String) -> Void
let openForgotPassword: () -> Void
let openSetupPassword: () -> Void
let openDisablePassword: () -> Void
let openSetupEmail: () -> Void
let openResetPendingEmail: () -> Void
init(updatePasswordText: @escaping (String) -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void) {
self.updatePasswordText = updatePasswordText
self.openForgotPassword = openForgotPassword
self.openSetupPassword = openSetupPassword
self.openDisablePassword = openDisablePassword
self.openSetupEmail = openSetupEmail
self.openResetPendingEmail = openResetPendingEmail
}
}
private enum TwoStepVerificationUnlockSettingsSection: Int32 {
case password
}
private enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag {
case password
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? TwoStepVerificationUnlockSettingsEntryTag {
switch self {
case .password:
if case .password = other {
return true
} else {
return false
}
}
} else {
return false
}
}
}
private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry {
case passwordEntry(PresentationTheme, String, String)
case passwordEntryInfo(PresentationTheme, String)
case passwordSetup(PresentationTheme, String)
case passwordSetupInfo(PresentationTheme, String)
case changePassword(PresentationTheme, String)
case turnPasswordOff(PresentationTheme, String)
case setupRecoveryEmail(PresentationTheme, String)
case passwordInfo(PresentationTheme, String)
case pendingEmailInfo(PresentationTheme, String)
var section: ItemListSectionId {
return TwoStepVerificationUnlockSettingsSection.password.rawValue
}
var stableId: Int32 {
switch self {
case .passwordEntry:
return 0
case .passwordEntryInfo:
return 1
case .passwordSetup:
return 2
case .passwordSetupInfo:
return 3
case .changePassword:
return 4
case .turnPasswordOff:
return 5
case .setupRecoveryEmail:
return 6
case .passwordInfo:
return 7
case .pendingEmailInfo:
return 8
}
}
static func ==(lhs: TwoStepVerificationUnlockSettingsEntry, rhs: TwoStepVerificationUnlockSettingsEntry) -> Bool {
switch lhs {
case let .passwordEntry(lhsTheme, lhsText, lhsValue):
if case let .passwordEntry(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .passwordEntryInfo(lhsTheme, lhsText):
if case let .passwordEntryInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .passwordSetupInfo(lhsTheme, lhsText):
if case let .passwordSetupInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .setupRecoveryEmail(lhsTheme, lhsText):
if case let .setupRecoveryEmail(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .passwordInfo(lhsTheme, lhsText):
if case let .passwordInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .pendingEmailInfo(lhsTheme, lhsText):
if case let .pendingEmailInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .passwordSetup(lhsTheme, lhsText):
if case let .passwordSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .changePassword(lhsTheme, lhsText):
if case let .changePassword(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .turnPasswordOff(lhsTheme, lhsText):
if case let .turnPasswordOff(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: TwoStepVerificationUnlockSettingsEntry, rhs: TwoStepVerificationUnlockSettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: TwoStepVerificationUnlockSettingsControllerArguments) -> ListViewItem {
switch self {
case let .passwordEntry(theme, text, value):
return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationUnlockSettingsEntryTag.password, sectionId: self.section, textUpdated: { updatedText in
arguments.updatePasswordText(updatedText)
}, action: {
})
case let .passwordEntryInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openForgotPassword()
}
})
case let .passwordSetup(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupPassword()
})
case let .passwordSetupInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .changePassword(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupPassword()
})
case let .turnPasswordOff(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openDisablePassword()
})
case let .setupRecoveryEmail(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupEmail()
})
case let .passwordInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .pendingEmailInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openResetPendingEmail()
}
})
}
}
}
private struct TwoStepVerificationUnlockSettingsControllerState: Equatable {
let passwordText: String
let checking: Bool
init(passwordText: String, checking: Bool) {
self.passwordText = passwordText
self.checking = checking
}
static func ==(lhs: TwoStepVerificationUnlockSettingsControllerState, rhs: TwoStepVerificationUnlockSettingsControllerState) -> Bool {
if lhs.passwordText != rhs.passwordText {
return false
}
if lhs.checking != rhs.checking {
return false
}
return true
}
func withUpdatedPasswordText(_ passwordText: String) -> TwoStepVerificationUnlockSettingsControllerState {
return TwoStepVerificationUnlockSettingsControllerState(passwordText: passwordText, checking: self.checking)
}
func withUpdatedChecking(_ cheking: Bool) -> TwoStepVerificationUnlockSettingsControllerState {
return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: cheking)
}
}
private func twoStepVerificationUnlockSettingsControllerEntries(presentationData: PresentationData, state: TwoStepVerificationUnlockSettingsControllerState,data: TwoStepVerificationUnlockSettingsControllerData) -> [TwoStepVerificationUnlockSettingsEntry] {
var entries: [TwoStepVerificationUnlockSettingsEntry] = []
switch data {
case let .access(configuration):
if let configuration = configuration {
switch configuration {
case let .notSet(pendingEmailPattern):
if pendingEmailPattern.isEmpty {
entries.append(.passwordSetup(presentationData.theme, presentationData.strings.TwoStepAuth_SetPassword))
entries.append(.passwordSetupInfo(presentationData.theme, presentationData.strings.TwoStepAuth_SetPasswordHelp))
} else {
entries.append(.pendingEmailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\n\(pendingEmailPattern)\n\n[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()"))
}
case let .set(hint, _, _):
entries.append(.passwordEntry(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordPassword, state.passwordText))
if hint.isEmpty {
entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)"))
} else {
entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHint(escapedPlaintextForMarkdown(hint)).0 + "\n\n" + presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)"))
}
}
}
case let .manage(_, emailSet, pendingEmailPattern):
entries.append(.changePassword(presentationData.theme, presentationData.strings.TwoStepAuth_ChangePassword))
entries.append(.turnPasswordOff(presentationData.theme, presentationData.strings.TwoStepAuth_RemovePassword))
entries.append(.setupRecoveryEmail(presentationData.theme, emailSet ? presentationData.strings.TwoStepAuth_ChangeEmail : presentationData.strings.TwoStepAuth_SetupEmail))
if pendingEmailPattern.isEmpty {
entries.append(.passwordInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHelp))
} else {
entries.append(.passwordInfo(presentationData.theme, presentationData.strings.TwoStepAuth_PendingEmailHelp(pendingEmailPattern).0))
}
}
return entries
}
enum TwoStepVerificationUnlockSettingsControllerMode {
case access
case manage(password: String, email: String, pendingEmailPattern: String)
}
private enum TwoStepVerificationUnlockSettingsControllerData {
case access(configuration: TwoStepVerificationConfiguration?)
case manage(password: String, emailSet: Bool, pendingEmailPattern: String)
}
func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStepVerificationUnlockSettingsControllerMode) -> ViewController {
let initialState = TwoStepVerificationUnlockSettingsControllerState(passwordText: "", checking: false)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((TwoStepVerificationUnlockSettingsControllerState) -> TwoStepVerificationUnlockSettingsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var replaceControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)?
let actionsDisposable = DisposableSet()
let checkDisposable = MetaDisposable()
actionsDisposable.add(checkDisposable)
let setupDisposable = MetaDisposable()
actionsDisposable.add(setupDisposable)
let setupResultDisposable = MetaDisposable()
actionsDisposable.add(setupResultDisposable)
let dataPromise = Promise<TwoStepVerificationUnlockSettingsControllerData>()
switch mode {
case .access:
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: nil)) |> then(twoStepVerificationConfiguration(account: account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: $0) }))
case let .manage(password, email, pendingEmailPattern):
dataPromise.set(.single(.manage(password: password, emailSet: !email.isEmpty, pendingEmailPattern: pendingEmailPattern)))
}
let arguments = TwoStepVerificationUnlockSettingsControllerArguments(updatePasswordText: { updatedText in
updateState {
$0.withUpdatedPasswordText(updatedText)
}
}, openForgotPassword: {
setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in
switch data {
case let .access(configuration):
if let configuration = configuration {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
switch configuration {
case let .set(_, hasRecoveryEmail, _):
if hasRecoveryEmail {
updateState {
$0.withUpdatedChecking(true)
}
setupResultDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(account: account) |> deliverOnMainQueue).start(next: { emailPattern in
updateState {
$0.withUpdatedChecking(false)
}
let result = Promise<Bool>()
let controller = twoStepVerificationResetController(account: account, emailPattern: emailPattern, result: result)
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
setupDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] _ in
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationConfiguration.notSet(pendingEmailPattern: ""))))
controller?.dismiss()
}))
}, error: { _ in
updateState {
$0.withUpdatedChecking(false)
}
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "An error occured. Please try again later.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
} else {
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
case .notSet:
break
}
}
case .manage:
break
}
}))
}, openSetupPassword: {
setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in
switch data {
case let .access(configuration):
if let configuration = configuration {
switch configuration {
case .notSet:
let result = Promise<TwoStepVerificationPasswordEntryResult?>()
let controller = twoStepVerificationPasswordEntryController(account: account, mode: .setup, result: result)
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in
if let updatedPassword = updatedPassword {
if let pendingEmailPattern = updatedPassword.pendingEmailPattern {
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationConfiguration.notSet(pendingEmailPattern: pendingEmailPattern))))
} else {
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: false, pendingEmailPattern: "")))
}
controller?.dismiss()
}
}))
case .set:
break
}
}
case let .manage(password, emailSet, pendingEmailPattern):
let result = Promise<TwoStepVerificationPasswordEntryResult?>()
let controller = twoStepVerificationPasswordEntryController(account: account, mode: .change(current: password), result: result)
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in
if let updatedPassword = updatedPassword {
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: emailSet, pendingEmailPattern: pendingEmailPattern)))
controller?.dismiss()
}
}))
}
}))
}, openDisablePassword: {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Are you sure you want to disable your password?", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: {
var disablePassword = false
updateState { state in
if state.checking {
return state
} else {
disablePassword = true
return state.withUpdatedChecking(true)
}
}
if disablePassword {
setupDisposable.set((dataPromise.get()
|> take(1)
|> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic }
|> mapToSignal { data -> Signal<Void, UpdateTwoStepVerificationPasswordError> in
switch data {
case .access:
return .complete()
case let .manage(password, _, _):
return updateTwoStepVerificationPassword(account: account, currentPassword: password, updatedPassword: .none)
|> mapToSignal { _ -> Signal<Void, UpdateTwoStepVerificationPasswordError> in
return .complete()
}
}
}
|> deliverOnMainQueue).start(error: { _ in
updateState {
$0.withUpdatedChecking(false)
}
}, completed: {
updateState {
$0.withUpdatedChecking(false)
}
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: .notSet(pendingEmailPattern: ""))))
}))
}
})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openSetupEmail: {
setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in
switch data {
case .access:
break
case let .manage(password, _, _):
let result = Promise<TwoStepVerificationPasswordEntryResult?>()
let controller = twoStepVerificationPasswordEntryController(account: account, mode: .setupEmail(password: password), result: result)
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in
if let updatedPassword = updatedPassword {
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: true, pendingEmailPattern: updatedPassword.pendingEmailPattern ?? "")))
controller?.dismiss()
}
}))
}
}))
}, openResetPendingEmail: {
updateState { state in
return state.withUpdatedChecking(true)
}
setupDisposable.set((updateTwoStepVerificationPassword(account: account, currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in
updateState { state in
return state.withUpdatedChecking(false)
}
dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: .notSet(pendingEmailPattern: ""))))
}, error: { _ in
updateState { state in
return state.withUpdatedChecking(false)
}
}))
})
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState<TwoStepVerificationUnlockSettingsEntry>, TwoStepVerificationUnlockSettingsEntry.ItemGenerationArguments)) in
var rightNavigationButton: ItemListNavigationButton?
var emptyStateItem: ItemListControllerEmptyStateItem?
let title: String
switch data {
case let .access(configuration):
title = presentationData.strings.TwoStepAuth_Title
if let configuration = configuration {
if state.checking {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
switch configuration {
case .notSet:
break
case .set:
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: true, action: {
var wasChecking = false
var password: String?
updateState { state in
wasChecking = state.checking
password = state.passwordText
return state.withUpdatedChecking(true)
}
if let password = password, !wasChecking {
checkDisposable.set((requestTwoStepVerifiationSettings(account: account, password: password) |> deliverOnMainQueue).start(next: { settings in
updateState {
$0.withUpdatedChecking(false)
}
replaceControllerImpl?(twoStepVerificationUnlockSettingsController(account: account, mode: .manage(password: password, email: settings.email, pendingEmailPattern: "")))
}, error: { error in
updateState {
$0.withUpdatedChecking(false)
}
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let text: String
switch error {
case .limitExceeded:
text = "You have entered invalid password too many times. Please try again later."
case .invalidPassword:
text = "Invalid password. Please try again."
case .generic:
text = "An error occured. Please try again later."
}
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}
})
}
}
} else {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
case .manage:
title = presentationData.strings.PrivacySettings_TwoStepAuth
if state.checking {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
}
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: TwoStepVerificationUnlockSettingsEntryTag.password, emptyStateItem: emptyStateItem, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(account: account, state: signal)
replaceControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: true)
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
return controller
}