1487 lines
71 KiB
Swift
1487 lines
71 KiB
Swift
//
|
|
// TwoStepVerificationUnlockController.swift
|
|
// Telegram
|
|
//
|
|
// Created by keepcoder on 16/10/2017.
|
|
// Copyright © 2017 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import TGUIKit
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
|
|
import Postbox
|
|
|
|
|
|
|
|
|
|
private struct TwoStepVerificationUnlockSettingsControllerState: Equatable {
|
|
let passwordText: String
|
|
let checking: Bool
|
|
let emailCode: String
|
|
let errors:[InputDataIdentifier : InputDataValueError]
|
|
let data: TwoStepVerificationUnlockSettingsControllerData
|
|
|
|
init(passwordText: String, checking: Bool, emailCode: String, errors: [InputDataIdentifier : InputDataValueError], data: TwoStepVerificationUnlockSettingsControllerData) {
|
|
self.passwordText = passwordText
|
|
self.checking = checking
|
|
self.emailCode = emailCode
|
|
self.errors = errors
|
|
self.data = data
|
|
}
|
|
|
|
func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationUnlockSettingsControllerState {
|
|
var errors = self.errors
|
|
if let error = error {
|
|
errors[key] = error
|
|
} else {
|
|
errors.removeValue(forKey: key)
|
|
}
|
|
return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: self.emailCode, errors: errors, data: self.data)
|
|
}
|
|
|
|
func withUpdatedPasswordText(_ passwordText: String) -> TwoStepVerificationUnlockSettingsControllerState {
|
|
return TwoStepVerificationUnlockSettingsControllerState(passwordText: passwordText, checking: self.checking, emailCode: self.emailCode, errors: self.errors, data: self.data)
|
|
}
|
|
func withUpdatedEmailCode(_ emailCode: String) -> TwoStepVerificationUnlockSettingsControllerState {
|
|
return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: emailCode, errors: self.errors, data: self.data)
|
|
}
|
|
|
|
func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationUnlockSettingsControllerState {
|
|
return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: checking, emailCode: self.emailCode, errors: self.errors, data: self.data)
|
|
}
|
|
|
|
func withUpdatedControllerData(_ data: TwoStepVerificationUnlockSettingsControllerData) -> TwoStepVerificationUnlockSettingsControllerState {
|
|
return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: self.emailCode, errors: self.errors, data: data)
|
|
}
|
|
}
|
|
|
|
|
|
enum TwoStepVerificationUnlockSettingsControllerMode {
|
|
case access(TwoStepVeriticationAccessConfiguration?)
|
|
case manage(password: String, email: String, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool)
|
|
}
|
|
|
|
private enum TwoStepVerificationUnlockSettingsControllerData : Equatable {
|
|
case access(configuration: TwoStepVeriticationAccessConfiguration?)
|
|
case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool)
|
|
}
|
|
|
|
|
|
|
|
struct PendingEmailState : Equatable {
|
|
let password: String?
|
|
let email: TwoStepVerificationPendingEmail
|
|
}
|
|
|
|
|
|
private final class TwoStepVerificationPasswordEntryControllerArguments {
|
|
let updateEntryText: (String) -> Void
|
|
let next: () -> Void
|
|
let skipEmail:() ->Void
|
|
init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, skipEmail:@escaping()->Void) {
|
|
self.updateEntryText = updateEntryText
|
|
self.next = next
|
|
self.skipEmail = skipEmail
|
|
}
|
|
}
|
|
|
|
|
|
|
|
enum PasswordEntryStage: Equatable {
|
|
case entry(text: String)
|
|
case reentry(first: String, text: String)
|
|
case hint(password: String, text: String)
|
|
case email(password: String, hint: String, text: String, change: Bool)
|
|
case code(text: String, codeLength: Int32?, pattern: String)
|
|
|
|
func updateCurrentText(_ text: String) -> PasswordEntryStage {
|
|
switch self {
|
|
case .entry:
|
|
return .entry(text: text)
|
|
case let .reentry(first, _):
|
|
return .reentry(first: first, text: text)
|
|
case let .hint(password, _):
|
|
return .hint(password: password, text: text)
|
|
case let .email(password, hint, _, change):
|
|
return .email(password: password, hint: hint, text: text, change: change)
|
|
case let .code(_, codeLength, pattern):
|
|
return .code(text: text, codeLength: codeLength, pattern: pattern)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private struct TwoStepVerificationPasswordEntryControllerState: Equatable {
|
|
let stage: PasswordEntryStage
|
|
let updating: Bool
|
|
let errors: [InputDataIdentifier : InputDataValueError]
|
|
init(stage: PasswordEntryStage, updating: Bool, errors: [InputDataIdentifier : InputDataValueError]) {
|
|
self.stage = stage
|
|
self.updating = updating
|
|
self.errors = errors
|
|
}
|
|
|
|
func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationPasswordEntryControllerState {
|
|
var errors = self.errors
|
|
if let error = error {
|
|
errors[key] = error
|
|
} else {
|
|
errors.removeValue(forKey: key)
|
|
}
|
|
return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: self.updating, errors: errors)
|
|
}
|
|
|
|
func withUpdatedStage(_ stage: PasswordEntryStage) -> TwoStepVerificationPasswordEntryControllerState {
|
|
return TwoStepVerificationPasswordEntryControllerState(stage: stage, updating: self.updating, errors: self.errors)
|
|
}
|
|
|
|
func withUpdatedUpdating(_ updating: Bool) -> TwoStepVerificationPasswordEntryControllerState {
|
|
return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: updating, errors: self.errors)
|
|
}
|
|
}
|
|
|
|
|
|
enum TwoStepVerificationPasswordEntryMode {
|
|
case setup
|
|
case change(current: String)
|
|
case setupEmail(password: String, change: Bool)
|
|
case enterCode(codeLength: Int32?, pattern: String)
|
|
}
|
|
|
|
|
|
enum TwoStepVeriticationAccessConfiguration : Equatable {
|
|
case notSet(pendingEmail: PendingEmailState?)
|
|
case set(hint: String, hasRecoveryEmail: Bool, hasSecureValues: Bool, pendingResetTimestamp: Int32?)
|
|
|
|
init(configuration: TwoStepVerificationConfiguration, password: String?) {
|
|
switch configuration {
|
|
case let .notSet(pendingEmail):
|
|
self = .notSet(pendingEmail: pendingEmail.flatMap({ PendingEmailState(password: password, email: $0) }))
|
|
case let .set(hint, hasRecoveryEmail, _, hasSecureValues, pendingResetTimestamp):
|
|
self = .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: pendingResetTimestamp)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum SetupTwoStepVerificationStateUpdate {
|
|
case noPassword
|
|
case awaitingEmailConfirmation(password: String, pattern: String, codeLength: Int32?)
|
|
case passwordSet(password: String?, hasRecoveryEmail: Bool, hasSecureValues: Bool)
|
|
case emailSet
|
|
}
|
|
|
|
|
|
|
|
|
|
final class TwoStepVerificationResetControllerArguments {
|
|
let updateEntryText: (String) -> Void
|
|
let next: () -> Void
|
|
let openEmailInaccessible: () -> Void
|
|
|
|
init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, openEmailInaccessible: @escaping () -> Void) {
|
|
self.updateEntryText = updateEntryText
|
|
self.next = next
|
|
self.openEmailInaccessible = openEmailInaccessible
|
|
}
|
|
}
|
|
|
|
|
|
struct TwoStepVerificationResetControllerState: Equatable {
|
|
let codeText: String
|
|
let checking: Bool
|
|
|
|
init(codeText: String, checking: Bool) {
|
|
self.codeText = codeText
|
|
self.checking = checking
|
|
}
|
|
|
|
|
|
func withUpdatedCodeText(_ codeText: String) -> TwoStepVerificationResetControllerState {
|
|
return TwoStepVerificationResetControllerState(codeText: codeText, checking: self.checking)
|
|
}
|
|
|
|
func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetControllerState {
|
|
return TwoStepVerificationResetControllerState(codeText: self.codeText, checking: checking)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private let _id_input_enter_pwd = InputDataIdentifier("input_password")
|
|
private let _id_change_pwd = InputDataIdentifier("change_pwd")
|
|
private let _id_remove_pwd = InputDataIdentifier("remove_pwd")
|
|
private let _id_setup_email = InputDataIdentifier("setup_email")
|
|
private let _id_enter_email_code = InputDataIdentifier("enter_email_code")
|
|
private let _id_set_password = InputDataIdentifier("set_password")
|
|
private let _id_input_enter_email_code = InputDataIdentifier("_id_input_enter_email_code")
|
|
|
|
private func twoStepVerificationUnlockSettingsControllerEntries(state: TwoStepVerificationUnlockSettingsControllerState, context: AccountContext, forgotPassword:@escaping()->Void, cancelReset:@escaping() -> Void, abort:@escaping()-> Void) -> [InputDataEntry] {
|
|
var entries: [InputDataEntry] = []
|
|
var sectionId:Int32 = 0
|
|
|
|
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
var index: Int32 = 0
|
|
|
|
|
|
switch state.data {
|
|
case let .access(configuration):
|
|
if let configuration = configuration {
|
|
switch configuration {
|
|
case let .notSet(pendingEmail):
|
|
if let pendingEmail = pendingEmail {
|
|
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(state.emailCode), error: state.errors[_id_input_enter_email_code], identifier: _id_input_enter_email_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: pendingEmail.email.codeLength ?? 255))
|
|
index += 1
|
|
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().twoStepAuthConfirmationTextNew + "\n\n\(pendingEmail.email.pattern)\n\n[" + strings().twoStepAuthConfirmationAbort + "]()", linkHandler: { url in
|
|
abort()
|
|
}), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem)))
|
|
index += 1
|
|
|
|
|
|
} else {
|
|
entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_set_password, data: InputDataGeneralData(name: strings().twoStepAuthSetPassword, color: theme.colors.text, icon: nil, type: .none, viewType: .singleItem, action: nil)))
|
|
index += 1
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthSetPasswordHelp), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
}
|
|
case let .set(hint, hasRecoveryEmail, _, pendingResetTimestamp):
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(state.passwordText), error: state.errors[_id_input_enter_pwd], identifier: _id_input_enter_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthEnterPasswordPassword, filter: { $0 }, limit: 255))
|
|
index += 1
|
|
if let timestamp = pendingResetTimestamp {
|
|
|
|
if timestamp.isFuture {
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().twoStepAuthEnterPasswordHelp + "\n\n" + strings().twoStepAuthResetPending(autoremoveLocalized(Int(timestamp - Int32(Date().timeIntervalSince1970)))) + "\n[" + strings().twoStepAuthCancelReset + "](reset)", linkHandler: { link in
|
|
verifyAlert_button(for: context.window, header: strings().twoStepAuthCancelResetConfirm, information: strings().twoStepAuthCancelResetText, ok: strings().alertYes, cancel: strings().alertNO, successHandler: { _ in
|
|
cancelReset()
|
|
})
|
|
}), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
} else {
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().twoStepAuthEnterPasswordHelp + "\n\n" + "[" + strings().twoStepAuthReset + "](reset)", linkHandler: { link in
|
|
forgotPassword()
|
|
}), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
}
|
|
index += 1
|
|
|
|
} else {
|
|
|
|
let forgot:()->Void = {
|
|
if !hasRecoveryEmail {
|
|
verifyAlert_button(for: context.window, header: strings().twoStepAuthErrorHaventEmailResetHeader, information: strings().twoStepAuthErrorHaventEmailNew, ok: strings().twoStepAuthErrorHaventEmailReset, successHandler: { _ in
|
|
forgotPassword()
|
|
})
|
|
} else {
|
|
forgotPassword()
|
|
}
|
|
}
|
|
|
|
if hint.isEmpty {
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().twoStepAuthEnterPasswordHelp + "\n\n[" + strings().twoStepAuthEnterPasswordForgot + "](forgot)", linkHandler: { link in
|
|
forgot()
|
|
}), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
} else {
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().twoStepAuthEnterPasswordHint(hint) + "\n\n" + strings().twoStepAuthEnterPasswordHelp + "\n\n[" + strings().twoStepAuthEnterPasswordForgot + "](forgot)", linkHandler: { link in
|
|
forgot()
|
|
}), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
}
|
|
} else {
|
|
return [.loading]
|
|
}
|
|
case let .manage(_, emailSet, pendingEmail, _):
|
|
|
|
entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_change_pwd, data: InputDataGeneralData(name: strings().twoStepAuthChangePassword, color: theme.colors.text, icon: nil, type: .none, viewType: .firstItem, action: nil)))
|
|
index += 1
|
|
entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_remove_pwd, data: InputDataGeneralData(name: strings().twoStepAuthRemovePassword, color: theme.colors.text, icon: nil, type: .none, viewType: .innerItem, action: nil)))
|
|
index += 1
|
|
entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_setup_email, data: InputDataGeneralData(name: emailSet ? strings().twoStepAuthChangeEmail : strings().twoStepAuthSetupEmail, color: theme.colors.text, icon: nil, type: .none, viewType: .lastItem, action: nil)))
|
|
index += 1
|
|
|
|
|
|
if let _ = pendingEmail {
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_enter_email_code, data: InputDataGeneralData(name: strings().twoStepAuthEnterEmailCode, color: theme.colors.text, icon: nil, type: .none, viewType: .singleItem, action: nil)))
|
|
index += 1
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthEmailSent), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
|
|
} else {
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthGenericHelp), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
}
|
|
|
|
}
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
|
|
return entries
|
|
}
|
|
|
|
|
|
|
|
|
|
func twoStepVerificationUnlockController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, presentController:@escaping((controller: ViewController, root:Bool, animated: Bool))->Void) -> InputDataController {
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
|
|
let checkDisposable = MetaDisposable()
|
|
actionsDisposable.add(checkDisposable)
|
|
|
|
let setupDisposable = MetaDisposable()
|
|
actionsDisposable.add(setupDisposable)
|
|
|
|
let setupResultDisposable = MetaDisposable()
|
|
actionsDisposable.add(setupResultDisposable)
|
|
|
|
|
|
let data: TwoStepVerificationUnlockSettingsControllerData
|
|
|
|
switch mode {
|
|
case let .access(configuration):
|
|
data = .access(configuration: configuration)
|
|
case let .manage(password, email, pendingEmail, hasSecureValues):
|
|
data = .manage(password: password, emailSet: !email.isEmpty, pendingEmail: pendingEmail, hasSecureValues: hasSecureValues)
|
|
}
|
|
//
|
|
let initialState = TwoStepVerificationUnlockSettingsControllerState(passwordText: "", checking: false, emailCode: "", errors: [:], data: data)
|
|
|
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
|
let stateValue = Atomic(value: initialState)
|
|
let updateState: ((TwoStepVerificationUnlockSettingsControllerState) -> TwoStepVerificationUnlockSettingsControllerState) -> Void = { f in
|
|
statePromise.set(stateValue.modify (f))
|
|
}
|
|
|
|
switch mode {
|
|
case .access:
|
|
actionsDisposable.add((context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVeriticationAccessConfiguration(configuration: $0, password: nil)) } |> deliverOnMainQueue).start(next: { data in
|
|
updateState {
|
|
$0.withUpdatedControllerData(data)
|
|
}
|
|
}))
|
|
default:
|
|
break
|
|
}
|
|
|
|
|
|
|
|
let disablePassword: () -> InputDataValidation = {
|
|
return .fail(.doSomething { f in
|
|
|
|
switch data {
|
|
case .access:
|
|
break
|
|
case let .manage(password, _, _, hasSecureValues):
|
|
|
|
var text: String = strings().twoStepAuthConfirmDisablePassword
|
|
if hasSecureValues {
|
|
text += "\n\n"
|
|
text += strings().secureIdWarningDataLost
|
|
}
|
|
|
|
verifyAlert_button(for: context.window, information: text, successHandler: { result in
|
|
var disablePassword = false
|
|
updateState { state in
|
|
if state.checking {
|
|
return state
|
|
} else {
|
|
disablePassword = true
|
|
return state.withUpdatedChecking(true)
|
|
}
|
|
}
|
|
context.hasPassportSettings.set(.single(false))
|
|
|
|
if disablePassword {
|
|
let resetPassword = context.engine.auth.updateTwoStepVerificationPassword(currentPassword: password, updatedPassword: .none) |> deliverOnMainQueue
|
|
|
|
setupDisposable.set(resetPassword.start(next: { value in
|
|
updateState {
|
|
$0.withUpdatedChecking(false)
|
|
}
|
|
context.resetTemporaryPwd()
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true))
|
|
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start()
|
|
}, error: { error in
|
|
alert(for: context.window, info: strings().unknownError)
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
let checkEmailConfirmation: () -> InputDataValidation = {
|
|
|
|
return .fail(.doSomething { f in
|
|
let data: TwoStepVerificationUnlockSettingsControllerData = stateValue.with { $0.data }
|
|
|
|
var pendingEmailData: PendingEmailState?
|
|
switch data {
|
|
case let .access(configuration):
|
|
guard let configuration = configuration else {
|
|
return
|
|
}
|
|
switch configuration {
|
|
case let .notSet(pendingEmail):
|
|
pendingEmailData = pendingEmail
|
|
case .set:
|
|
break
|
|
}
|
|
case let .manage(password, _, pendingEmail, _):
|
|
if let pendingEmail = pendingEmail {
|
|
pendingEmailData = PendingEmailState(password: password, email: pendingEmail)
|
|
}
|
|
}
|
|
if let pendingEmail = pendingEmailData {
|
|
var code: String?
|
|
updateState { state in
|
|
if !state.checking {
|
|
code = state.emailCode
|
|
return state.withUpdatedChecking(true)
|
|
}
|
|
return state
|
|
}
|
|
if let code = code {
|
|
setupDisposable.set((context.engine.auth.confirmTwoStepRecoveryEmail(code: code)
|
|
|> deliverOnMainQueue).start(error: { error in
|
|
updateState { state in
|
|
return state.withUpdatedChecking(false)
|
|
}
|
|
let text: String
|
|
switch error {
|
|
case .invalidEmail:
|
|
text = strings().twoStepAuthEmailInvalid
|
|
case .invalidCode:
|
|
text = strings().twoStepAuthEmailCodeInvalid
|
|
case .expired:
|
|
text = strings().twoStepAuthEmailCodeExpired
|
|
case .flood:
|
|
text = strings().twoStepAuthFloodError
|
|
case .generic:
|
|
text = strings().unknownError
|
|
}
|
|
updateState {
|
|
$0.withUpdatedError(InputDataValueError(description: text, target: .data), for: _id_input_enter_email_code)
|
|
}
|
|
f(.fail(.fields([_id_input_enter_email_code:.shake])))
|
|
}, completed: {
|
|
switch data {
|
|
case .access:
|
|
if let password = pendingEmail.password {
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "", pendingEmail: nil, hasSecureValues: false), presentController: presentController), root: true, animated: true))
|
|
} else {
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.set(hint: "", hasRecoveryEmail: true, hasSecureValues: false, pendingResetTimestamp: nil)), presentController: presentController), root: true, animated: true))
|
|
}
|
|
case let .manage(manage):
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: manage.password, email: "", pendingEmail: nil, hasSecureValues: manage.hasSecureValues), presentController: presentController), root: true, animated: true))
|
|
}
|
|
|
|
updateState { state in
|
|
return state.withUpdatedChecking(false).withUpdatedEmailCode("")
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
|
|
|
|
|
|
let validateAccessPassword:([InputDataIdentifier : InputDataValue]) -> InputDataValidation = { data in
|
|
var wasChecking: Bool = false
|
|
updateState { state in
|
|
wasChecking = state.checking
|
|
return state
|
|
}
|
|
|
|
updateState { state in
|
|
return state.withUpdatedChecking(!wasChecking)
|
|
}
|
|
|
|
if !wasChecking, let password = data[_id_input_enter_pwd]?.stringValue {
|
|
|
|
return .fail(.doSomething(next: { f in
|
|
|
|
checkDisposable.set((context.engine.auth.requestTwoStepVerifiationSettings(password: password)
|
|
|> mapToSignal { settings -> Signal<(TwoStepVerificationSettings, TwoStepVerificationPendingEmail?), AuthorizationPasswordVerificationError> in
|
|
return context.engine.auth.twoStepVerificationConfiguration()
|
|
|> castError(AuthorizationPasswordVerificationError.self)
|
|
|> map { configuration in
|
|
var pendingEmail: TwoStepVerificationPendingEmail?
|
|
if case let .set(configuration) = configuration {
|
|
pendingEmail = configuration.pendingEmail
|
|
}
|
|
return (settings, pendingEmail)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { settings, pendingEmail in
|
|
updateState {
|
|
$0.withUpdatedChecking(false)
|
|
}
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil), presentController: presentController), root: true, animated: true))
|
|
f(.none)
|
|
}, error: { error in
|
|
let text: String
|
|
switch error {
|
|
case .limitExceeded:
|
|
text = strings().twoStepAuthErrorLimitExceeded
|
|
case .invalidPassword:
|
|
text = strings().twoStepAuthInvalidPasswordError
|
|
case .generic:
|
|
text = strings().twoStepAuthErrorGeneric
|
|
}
|
|
updateState {
|
|
$0.withUpdatedChecking(false).withUpdatedError(InputDataValueError(description: text, target: .data), for: _id_input_enter_pwd)
|
|
}
|
|
|
|
f(.fail(.fields([_id_input_enter_pwd : .shake])))
|
|
|
|
}))
|
|
|
|
}))
|
|
|
|
|
|
} else {
|
|
checkDisposable.set(nil)
|
|
}
|
|
|
|
return .none
|
|
}
|
|
|
|
let proccessEntryResult:(SetupTwoStepVerificationStateUpdate) -> Void = { update in
|
|
switch update {
|
|
case .noPassword:
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true))
|
|
case let .awaitingEmailConfirmation(password, pattern, codeLength):
|
|
|
|
let data = stateValue.with {$0.data}
|
|
|
|
let hasSecureValues: Bool
|
|
|
|
switch data {
|
|
case let .manage(_, _, _, _hasSecureValues):
|
|
hasSecureValues = _hasSecureValues
|
|
case .access:
|
|
hasSecureValues = false
|
|
}
|
|
|
|
|
|
|
|
let pendingEmail = TwoStepVerificationPendingEmail(pattern: pattern, codeLength: codeLength)
|
|
|
|
let root = twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "", pendingEmail: pendingEmail, hasSecureValues: hasSecureValues), presentController: presentController)
|
|
|
|
presentController((controller: root, root: true, animated: false))
|
|
|
|
presentController((controller: twoStepVerificationPasswordEntryController(context: context, mode: .enterCode(codeLength: pendingEmail.codeLength, pattern: pendingEmail.pattern), initialStage: nil, result: { _ in
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "email", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true))
|
|
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start()
|
|
}, presentController: presentController), root: false, animated: true))
|
|
|
|
|
|
case .emailSet:
|
|
let data = stateValue.with {$0.data}
|
|
|
|
switch data {
|
|
case let .manage(password, _, _, hasSecureValues):
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "email", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true))
|
|
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start()
|
|
default:
|
|
break
|
|
}
|
|
case let .passwordSet(password, hasRecoveryEmail, hasSecureValues):
|
|
if let password = password {
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: hasRecoveryEmail ? "email" : "", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true))
|
|
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start()
|
|
} else {
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.set(hint: "", hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: nil)), presentController: presentController), root: true, animated: true))
|
|
}
|
|
}
|
|
}
|
|
|
|
let setupPassword:() -> InputDataValidation = {
|
|
let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setup, initialStage: nil, result: proccessEntryResult, presentController: presentController)
|
|
presentController((controller: controller, root: false, animated: true))
|
|
return .none
|
|
}
|
|
|
|
let changePassword: (_ current: String) -> InputDataValidation = { current in
|
|
let controller = twoStepVerificationPasswordEntryController(context: context, mode: .change(current: current), initialStage: nil, result: proccessEntryResult, presentController: presentController)
|
|
presentController((controller: controller, root: false, animated: true))
|
|
return .none
|
|
}
|
|
|
|
let setupRecoveryEmail:() -> InputDataValidation = {
|
|
|
|
let data = stateValue.with {$0.data}
|
|
|
|
switch data {
|
|
case .access:
|
|
break
|
|
case let .manage(password, emailSet, _, _):
|
|
let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setupEmail(password: password, change: emailSet), initialStage: nil, result: proccessEntryResult, presentController: presentController)
|
|
presentController((controller: controller, root: false, animated: true))
|
|
}
|
|
|
|
return .none
|
|
}
|
|
|
|
let enterCode:() -> InputDataValidation = {
|
|
let data = stateValue.with {$0.data}
|
|
|
|
switch data {
|
|
case .access:
|
|
break
|
|
case let .manage(_, _, pendingEmail, _):
|
|
if let pendingEmail = pendingEmail {
|
|
let controller = twoStepVerificationPasswordEntryController(context: context, mode: .enterCode(codeLength: pendingEmail.codeLength, pattern: pendingEmail.pattern), initialStage: nil, result: proccessEntryResult, presentController: presentController)
|
|
presentController((controller: controller, root: false, animated: true))
|
|
}
|
|
}
|
|
|
|
return .none
|
|
}
|
|
|
|
let cancelReset: () -> Void = {
|
|
let _ = (context.engine.auth.declineTwoStepPasswordReset()
|
|
|> deliverOnMainQueue).start(completed: {
|
|
|
|
_ = showModalProgress(signal: context.engine.auth.twoStepVerificationConfiguration(), for: context.window).start(next: { configuration in
|
|
updateState {
|
|
$0.withUpdatedControllerData(.access(configuration: .init(configuration: configuration, password: nil)))
|
|
}
|
|
})
|
|
|
|
})
|
|
}
|
|
|
|
|
|
let forgotPassword:() -> Void = {
|
|
|
|
let data = stateValue.with {$0.data}
|
|
switch data {
|
|
case let .access(configuration):
|
|
if let configuration = configuration {
|
|
switch configuration {
|
|
case let .set(hint, hasRecoveryEmail, hasSecureValues, _):
|
|
if hasRecoveryEmail {
|
|
updateState { state in
|
|
return state.withUpdatedChecking(true)
|
|
}
|
|
|
|
setupResultDisposable.set((context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
|
|
|> deliverOnMainQueue).start(next: { emailPattern in
|
|
|
|
updateState { state in
|
|
return state.withUpdatedChecking(false)
|
|
}
|
|
|
|
presentController((controller: twoStepVerificationResetPasswordController(context: context, emailPattern: emailPattern, success: {
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true))
|
|
}), root: false, animated: true))
|
|
|
|
}, error: { _ in
|
|
updateState { state in
|
|
return state.withUpdatedChecking(false)
|
|
}
|
|
alert(for: context.window, info: strings().twoStepAuthAnError)
|
|
}))
|
|
} else {
|
|
|
|
let reset:()->Void = {
|
|
_ = showModalProgress(signal: context.engine.auth.requestTwoStepPasswordReset(), for: context.window).start(next: { result in
|
|
switch result {
|
|
case .done:
|
|
updateState {
|
|
$0.withUpdatedControllerData(.access(configuration: .notSet(pendingEmail: nil)))
|
|
}
|
|
verifyAlert_button(for: context.window, header: strings().twoStepAuthResetSuccessHeader, information: strings().twoStepAuthResetSuccess, ok: strings().alertYes, cancel: strings().alertNO, successHandler: { _ in
|
|
let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setup, initialStage: nil, result: proccessEntryResult, presentController: presentController)
|
|
presentController((controller: controller, root: true, animated: true))
|
|
})
|
|
case let .error(reason):
|
|
switch reason {
|
|
case let .limitExceeded(retryin):
|
|
if let retryin = retryin {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
formatter.timeStyle = .medium
|
|
formatter.timeZone = NSTimeZone.local
|
|
alert(for: context.window, info: strings().twoStepAuthUnableToReset(formatter.string(from: Date.init(timeIntervalSince1970: TimeInterval(retryin)))))
|
|
} else {
|
|
alert(for: context.window, info: strings().errorAnError)
|
|
}
|
|
default:
|
|
alert(for: context.window, info: strings().errorAnError)
|
|
}
|
|
case .declined:
|
|
break
|
|
case let .waitingForReset(resetAtTimestamp):
|
|
updateState {
|
|
$0.withUpdatedControllerData(.access(configuration: .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: resetAtTimestamp)))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
reset()
|
|
|
|
}
|
|
|
|
default:
|
|
break
|
|
}
|
|
|
|
}
|
|
case .manage:
|
|
break
|
|
}
|
|
|
|
}
|
|
|
|
let abort: () -> Void = {
|
|
updateState { $0.withUpdatedChecking(true) }
|
|
let resetPassword = context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue
|
|
|
|
setupDisposable.set(resetPassword.start(next: { value in
|
|
updateState { $0.withUpdatedChecking(false) }
|
|
presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true))
|
|
}, error: { error in
|
|
alert(for: context.window, info: strings().unknownError)
|
|
}))
|
|
}
|
|
|
|
let _repeat:Signal<Void, NoError> = (.single(Void()) |> then(.single(Void()) |> suspendAwareDelay(1, queue: Queue.concurrentDefaultQueue()))) |> restart
|
|
|
|
|
|
let signal: Signal<[InputDataEntry], NoError> = combineLatest(statePromise.get(), _repeat) |> map { state, _ -> [InputDataEntry] in
|
|
return twoStepVerificationUnlockSettingsControllerEntries(state: state, context: context, forgotPassword: forgotPassword, cancelReset: cancelReset, abort: abort)
|
|
}
|
|
|
|
|
|
return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: strings().privacySettingsTwoStepVerification, validateData: { validateData -> InputDataValidation in
|
|
|
|
let data = stateValue.with {$0.data}
|
|
let loading = stateValue.with {$0.checking}
|
|
|
|
if !loading {
|
|
switch mode {
|
|
case .access:
|
|
switch data {
|
|
case let .access(configuration):
|
|
if let configuration = configuration {
|
|
switch configuration {
|
|
case let .notSet(pendingEmail):
|
|
if let _ = pendingEmail {
|
|
return checkEmailConfirmation()
|
|
} else {
|
|
return setupPassword()
|
|
}
|
|
case .set:
|
|
return validateAccessPassword(validateData)
|
|
}
|
|
}
|
|
case .manage:
|
|
break
|
|
}
|
|
case let .manage(password, _, _, _):
|
|
if let _ = validateData[_id_remove_pwd] {
|
|
return disablePassword()
|
|
} else if let _ = validateData[_id_change_pwd] {
|
|
return changePassword(password)
|
|
} else if let _ = validateData[_id_setup_email] {
|
|
return setupRecoveryEmail()
|
|
} else if let _ = validateData[_id_enter_email_code] {
|
|
return enterCode()
|
|
}
|
|
|
|
}
|
|
} else {
|
|
NSSound.beep()
|
|
}
|
|
|
|
return .none
|
|
|
|
}, updateDatas: { data in
|
|
if let password = data[_id_input_enter_pwd]?.stringValue {
|
|
updateState { state in
|
|
return state.withUpdatedPasswordText(password).withUpdatedError(nil, for: _id_input_enter_pwd)
|
|
}
|
|
} else if let code = data[_id_input_enter_email_code]?.stringValue {
|
|
updateState { state in
|
|
return state.withUpdatedEmailCode(code).withUpdatedError(nil, for: _id_input_enter_email_code)
|
|
}
|
|
}
|
|
return .none
|
|
}, afterDisappear: {
|
|
actionsDisposable.dispose()
|
|
}, updateDoneValue: { data in
|
|
return { f in
|
|
|
|
let data = stateValue.with {$0.data}
|
|
|
|
switch mode {
|
|
case .access:
|
|
switch data {
|
|
case let .access(configuration: configuration):
|
|
if let configuration = configuration {
|
|
switch configuration {
|
|
case let .notSet(pendingEmail):
|
|
if let _ = pendingEmail {
|
|
var checking: Bool = false
|
|
var codeEmpty: Bool = true
|
|
updateState { state in
|
|
checking = state.checking
|
|
codeEmpty = state.emailCode.isEmpty
|
|
return state
|
|
}
|
|
return f(checking ? .loading : codeEmpty ? .disabled(strings().navigationDone) : .enabled(strings().navigationDone))
|
|
} else {
|
|
|
|
}
|
|
case .set:
|
|
var checking: Bool = false
|
|
var pwdEmpty: Bool = true
|
|
updateState { state in
|
|
checking = state.checking
|
|
pwdEmpty = state.passwordText.isEmpty
|
|
return state
|
|
}
|
|
return f(checking ? .loading : pwdEmpty ? .disabled(strings().navigationDone) : .enabled(strings().navigationDone))
|
|
}
|
|
} else {
|
|
return f(.invisible)
|
|
}
|
|
case .manage:
|
|
break
|
|
}
|
|
|
|
default:
|
|
break
|
|
}
|
|
|
|
var checking: Bool = false
|
|
updateState { state in
|
|
checking = state.checking
|
|
return state
|
|
}
|
|
return f(checking ? .loading : .invisible)
|
|
|
|
}
|
|
}, removeAfterDisappear: false, hasDone: true, identifier: "tsv-unlock")
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private struct TwoStepVerificationResetState : Equatable {
|
|
let code: String
|
|
let checking: Bool
|
|
let emailPattern: String
|
|
let errors: [InputDataIdentifier : InputDataValueError]
|
|
init(emailPattern: String, code: String, checking: Bool, errors: [InputDataIdentifier : InputDataValueError] = [:]) {
|
|
self.code = code
|
|
self.checking = checking
|
|
self.emailPattern = emailPattern
|
|
self.errors = errors
|
|
}
|
|
|
|
func withUpdatedCode(_ code: String) -> TwoStepVerificationResetState {
|
|
return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: code, checking: self.checking, errors: self.errors)
|
|
}
|
|
func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetState {
|
|
return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: self.code, checking: checking, errors: self.errors)
|
|
}
|
|
func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationResetState {
|
|
var errors = self.errors
|
|
if let error = error {
|
|
errors[key] = error
|
|
} else {
|
|
errors.removeValue(forKey: key)
|
|
}
|
|
return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: self.code, checking: checking, errors: errors)
|
|
}
|
|
}
|
|
|
|
private let _id_input_recovery_code = InputDataIdentifier("_id_input_recovery_code")
|
|
|
|
private func twoStepVerificationResetPasswordEntries( state: TwoStepVerificationResetState, unavailable: @escaping()-> Void) -> [InputDataEntry] {
|
|
|
|
var entries: [InputDataEntry] = []
|
|
|
|
var sectionId: Int32 = 0
|
|
var index: Int32 = 0
|
|
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(state.code), error: state.errors[_id_input_recovery_code], identifier: _id_input_recovery_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 255))
|
|
index += 1
|
|
|
|
let info = strings().twoStepAuthRecoveryCodeHelp + "\n\n\(strings().twoStepAuthRecoveryEmailUnavailableNew(state.emailPattern))"
|
|
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(info, linkHandler: { _ in
|
|
unavailable()
|
|
}), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem)))
|
|
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
return entries
|
|
}
|
|
|
|
|
|
|
|
private func twoStepVerificationResetPasswordController(context: AccountContext, emailPattern: String, success: @escaping()->Void) -> InputDataController {
|
|
|
|
|
|
|
|
let initialState = TwoStepVerificationResetState(emailPattern: emailPattern, code: "", checking: false)
|
|
|
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
|
let stateValue = Atomic(value: initialState)
|
|
let updateState: ((TwoStepVerificationResetState) -> TwoStepVerificationResetState) -> Void = { f in
|
|
statePromise.set(stateValue.modify(f))
|
|
}
|
|
|
|
let resetDisposable = MetaDisposable()
|
|
|
|
let signal: Signal<[InputDataEntry], NoError> = statePromise.get() |> map { state in
|
|
return twoStepVerificationResetPasswordEntries(state: state, unavailable: {
|
|
alert(for: context.window, info: strings().twoStepAuthRecoveryFailed)
|
|
})
|
|
}
|
|
|
|
let checkRecoveryCode: (String) -> InputDataValidation = { code in
|
|
return .fail(.doSomething { f in
|
|
|
|
updateState {
|
|
return $0.withUpdatedChecking(true)
|
|
}
|
|
|
|
resetDisposable.set((context.engine.auth.performPasswordRecovery(code: code, updatedPassword: .none) |> deliverOnMainQueue).start(error: { error in
|
|
|
|
let errorText: String
|
|
switch error {
|
|
case .generic:
|
|
errorText = strings().twoStepAuthGenericError
|
|
case .invalidCode:
|
|
errorText = strings().twoStepAuthRecoveryCodeInvalid
|
|
case .expired:
|
|
errorText = strings().twoStepAuthRecoveryCodeExpired
|
|
case .limitExceeded:
|
|
errorText = strings().twoStepAuthFloodError
|
|
}
|
|
|
|
updateState {
|
|
return $0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_recovery_code).withUpdatedChecking(false)
|
|
}
|
|
|
|
f(.fail(.fields([_id_input_recovery_code: .shake])))
|
|
|
|
}, completed: {
|
|
updateState {
|
|
return $0.withUpdatedChecking(false)
|
|
}
|
|
success()
|
|
}))
|
|
})
|
|
}
|
|
|
|
return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: strings().twoStepAuthRecoveryTitle, validateData: { data in
|
|
|
|
let code = stateValue.with {$0.code}
|
|
let loading = stateValue.with {$0.checking}
|
|
|
|
if !loading {
|
|
return checkRecoveryCode(code)
|
|
} else {
|
|
NSSound.beep()
|
|
}
|
|
return .none
|
|
}, updateDatas: { data in
|
|
updateState { current in
|
|
return current.withUpdatedCode(data[_id_input_recovery_code]?.stringValue ?? current.code).withUpdatedError(nil, for: _id_input_recovery_code)
|
|
}
|
|
return .none
|
|
}, afterDisappear: {
|
|
resetDisposable.dispose()
|
|
}, updateDoneValue: { data in
|
|
return { f in
|
|
let code = stateValue.with {$0.code}
|
|
let loading = stateValue.with {$0.checking}
|
|
f(loading ? .loading : code.isEmpty ? .disabled(strings().navigationDone) : .enabled(strings().navigationDone))
|
|
}
|
|
}, removeAfterDisappear: true, hasDone: true, identifier: "tsv-reset")
|
|
}
|
|
|
|
|
|
|
|
|
|
private let _id_input_entry_pwd = InputDataIdentifier("_id_input_entry_pwd")
|
|
private let _id_input_reentry_pwd = InputDataIdentifier("_id_input_reentry_pwd")
|
|
private let _id_input_entry_hint = InputDataIdentifier("_id_input_entry_hint")
|
|
private let _id_input_entry_email = InputDataIdentifier("_id_input_entry_email")
|
|
private let _id_input_entry_code = InputDataIdentifier("_id_input_entry_code")
|
|
|
|
private func twoStepVerificationPasswordEntryControllerEntries(state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [InputDataEntry] {
|
|
var entries: [InputDataEntry] = []
|
|
|
|
var sectionId:Int32 = 0
|
|
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
var index: Int32 = 0
|
|
|
|
|
|
switch state.stage {
|
|
case let .entry(text):
|
|
|
|
let placeholder:String
|
|
switch mode {
|
|
case .change:
|
|
placeholder = strings().twoStepAuthEnterPasswordPassword
|
|
default:
|
|
placeholder = strings().twoStepAuthEnterPasswordPassword
|
|
}
|
|
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_pwd], identifier: _id_input_entry_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: placeholder, filter: { $0 }, limit: 255))
|
|
index += 1
|
|
|
|
switch mode {
|
|
case .setup:
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthSetupPasswordDesc), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
case .change:
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthChangePasswordDesc), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
default:
|
|
break
|
|
}
|
|
|
|
case let .reentry(_, text):
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_reentry_pwd], identifier: _id_input_reentry_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthEnterPasswordPassword, filter: { $0 }, limit: 255))
|
|
index += 1
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthSetupPasswordConfirmPassword), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
case let .hint(_, text):
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_hint], identifier: _id_input_entry_hint, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthSetupHintPlaceholder, filter: { $0 }, limit: 255))
|
|
index += 1
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthSetupHintDesc), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
index += 1
|
|
case let .email(_, _, text, change):
|
|
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_email], identifier: _id_input_entry_email, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthEmail, filter: { $0 }, limit: 255))
|
|
index += 1
|
|
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(change ? strings().twoStepAuthEmailHelpChange : strings().twoStepAuthEmailHelp), data: InputDataGeneralTextData(viewType: .textBottomItem)))
|
|
case let .code(text, codeLength, pattern):
|
|
entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_code], identifier: _id_input_entry_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: codeLength ?? 255))
|
|
index += 1
|
|
|
|
entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().twoStepAuthConfirmEmailCodeDesc(pattern)), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem)))
|
|
}
|
|
|
|
entries.append(.sectionId(sectionId, type: .normal))
|
|
sectionId += 1
|
|
|
|
return entries
|
|
}
|
|
|
|
|
|
|
|
func twoStepVerificationPasswordEntryController(context: AccountContext, mode: TwoStepVerificationPasswordEntryMode, initialStage: PasswordEntryStage?, result: @escaping(SetupTwoStepVerificationStateUpdate) -> Void, presentController: @escaping((controller: ViewController, root: Bool, animated: Bool)) -> Void) -> InputDataController {
|
|
|
|
|
|
let network = context.account.network
|
|
|
|
var initialStage: PasswordEntryStage! = initialStage
|
|
if initialStage == nil {
|
|
switch mode {
|
|
case .setup, .change:
|
|
initialStage = .entry(text: "")
|
|
case let .setupEmail(password, change):
|
|
initialStage = .email(password: password, hint: "", text: "", change: change)
|
|
case let .enterCode(codeLength, pattern):
|
|
initialStage = .code(text: "", codeLength: codeLength, pattern: pattern)
|
|
}
|
|
}
|
|
|
|
let initialState = TwoStepVerificationPasswordEntryControllerState(stage: initialStage, updating: false, errors: [:])
|
|
|
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
|
let stateValue = Atomic(value: initialState)
|
|
let updateState: ((TwoStepVerificationPasswordEntryControllerState) -> TwoStepVerificationPasswordEntryControllerState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
let signal: Signal<[InputDataEntry], NoError> = statePromise.get() |> map { state in
|
|
return twoStepVerificationPasswordEntryControllerEntries(state: state, mode: mode)
|
|
}
|
|
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
let updatePasswordDisposable = MetaDisposable()
|
|
actionsDisposable.add(updatePasswordDisposable)
|
|
|
|
|
|
func checkAndSaveState(context:AccountContext) -> InputDataValidation {
|
|
var passwordHintEmail: (String, String, String)?
|
|
var enterCode: String?
|
|
updateState { state in
|
|
if state.updating {
|
|
return state
|
|
} else {
|
|
switch state.stage {
|
|
case .entry:
|
|
break
|
|
case .reentry:
|
|
break
|
|
case let .hint(password, text):
|
|
switch mode {
|
|
case .change:
|
|
passwordHintEmail = (password, text, "")
|
|
default:
|
|
preconditionFailure()
|
|
}
|
|
case let .email(password, hint, text, _):
|
|
passwordHintEmail = (password, hint, text)
|
|
case let .code(text, _, _):
|
|
enterCode = text
|
|
}
|
|
}
|
|
return state
|
|
}
|
|
|
|
|
|
return .fail(.doSomething { f in
|
|
if let (password, hint, email) = passwordHintEmail {
|
|
|
|
updateState {
|
|
$0.withUpdatedUpdating(true)
|
|
}
|
|
|
|
switch mode {
|
|
case .setup, .change:
|
|
var currentPassword: String?
|
|
if case let .change(current) = mode {
|
|
currentPassword = current
|
|
}
|
|
|
|
updatePasswordDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .password(password: password, hint: hint, email: email)) |> deliverOnMainQueue).start(next: { update in
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
switch update {
|
|
case let .password(password, pendingEmail):
|
|
if let pendingEmail = pendingEmail {
|
|
result(.awaitingEmailConfirmation(password: password, pattern: email, codeLength: pendingEmail.codeLength))
|
|
} else {
|
|
result(.passwordSet(password: password, hasRecoveryEmail: false, hasSecureValues: false))
|
|
}
|
|
case .none:
|
|
break
|
|
}
|
|
}, error: { error in
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
switch error {
|
|
case .generic:
|
|
alert(for: context.window, info: strings().twoStepAuthErrorGeneric)
|
|
case .invalidEmail:
|
|
updateState {
|
|
$0.withUpdatedError(InputDataValueError(description: strings().twoStepAuthErrorInvalidEmail, target: .data), for: _id_input_entry_email)
|
|
}
|
|
f(.fail(.fields([_id_input_entry_email: .shake])))
|
|
}
|
|
|
|
}))
|
|
case let .setupEmail(password, _):
|
|
updatePasswordDisposable.set((context.engine.auth.updateTwoStepVerificationEmail(currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { update in
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
switch update {
|
|
case let .password(password, pendingEmail):
|
|
if let pendingEmail = pendingEmail {
|
|
result(.awaitingEmailConfirmation(password: password, pattern: email, codeLength: pendingEmail.codeLength))
|
|
} else {
|
|
result(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false))
|
|
}
|
|
case .none:
|
|
break
|
|
}
|
|
}, error: { error in
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
let errorText: String
|
|
switch error {
|
|
case .generic:
|
|
errorText = strings().twoStepAuthErrorGeneric
|
|
case .invalidEmail:
|
|
errorText = strings().twoStepAuthErrorInvalidEmail
|
|
}
|
|
updateState {
|
|
$0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_entry_email)
|
|
}
|
|
f(.fail(.fields([_id_input_entry_email: .shake])))
|
|
}))
|
|
case .enterCode:
|
|
fatalError()
|
|
}
|
|
} else if let code = enterCode {
|
|
updateState {
|
|
$0.withUpdatedUpdating(true)
|
|
}
|
|
updatePasswordDisposable.set((context.engine.auth.confirmTwoStepRecoveryEmail(code: code) |> deliverOnMainQueue).start(error: { error in
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
let errorText: String
|
|
switch error {
|
|
case .generic:
|
|
errorText = strings().twoStepAuthGenericError
|
|
case .invalidCode:
|
|
errorText = strings().twoStepAuthRecoveryCodeInvalid
|
|
case .expired:
|
|
errorText = strings().twoStepAuthRecoveryCodeExpired
|
|
case .flood:
|
|
errorText = strings().twoStepAuthFloodError
|
|
case .invalidEmail:
|
|
errorText = strings().twoStepAuthErrorInvalidEmail
|
|
}
|
|
updateState {
|
|
$0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_entry_code)
|
|
}
|
|
f(.fail(.fields([_id_input_entry_code: .shake])))
|
|
|
|
}, completed: {
|
|
updateState {
|
|
$0.withUpdatedUpdating(false)
|
|
}
|
|
result(.emailSet)
|
|
}))
|
|
}
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: "", validateData: { data -> InputDataValidation in
|
|
|
|
var stage: PasswordEntryStage?
|
|
var allowPerform: Bool = true
|
|
|
|
let loading = stateValue.with {$0.updating}
|
|
|
|
if !loading {
|
|
return .fail(.doSomething { f in
|
|
var skipEmail: Bool = false
|
|
updateState { state in
|
|
var state = state
|
|
if state.updating {
|
|
return state
|
|
} else {
|
|
switch state.stage {
|
|
case let .entry(text):
|
|
if text.isEmpty {
|
|
return state
|
|
} else {
|
|
stage = .reentry(first: text, text: "")
|
|
}
|
|
case let .reentry(first, text):
|
|
if text.isEmpty {
|
|
|
|
} else if text != first {
|
|
state = state.withUpdatedError(InputDataValueError(description: strings().twoStepAuthSetupPasswordConfirmFailed, target: .data), for: _id_input_reentry_pwd)
|
|
f(.fail(.fields([_id_input_reentry_pwd : .shake])))
|
|
} else {
|
|
stage = .hint(password: text, text: "")
|
|
}
|
|
case let .hint(password, text):
|
|
switch mode {
|
|
case .setup:
|
|
stage = .email(password: password, hint: text, text: "", change: false)
|
|
default:
|
|
break
|
|
}
|
|
case let .email(_, _, text, _):
|
|
if text.isEmpty {
|
|
skipEmail = true
|
|
}
|
|
case let .code(text, codeLength, _):
|
|
if text.isEmpty {
|
|
allowPerform = false
|
|
} else if let codeLength = codeLength, text.length != codeLength {
|
|
allowPerform = false
|
|
} else {
|
|
allowPerform = true
|
|
}
|
|
}
|
|
return state
|
|
}
|
|
}
|
|
if allowPerform {
|
|
if let stage = stage {
|
|
presentController((controller: twoStepVerificationPasswordEntryController(context: context, mode: mode, initialStage: stage, result: result, presentController: presentController), root: false, animated: true))
|
|
} else {
|
|
if skipEmail {
|
|
verifyAlert_button(for: context.window, information: strings().twoStepAuthEmailSkipAlert, ok: strings().twoStepAuthEmailSkip, successHandler: { _ in
|
|
f(checkAndSaveState(context: context))
|
|
})
|
|
} else {
|
|
f(checkAndSaveState(context: context))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
NSSound.beep()
|
|
return .none
|
|
}
|
|
}, updateDatas: { data -> InputDataValidation in
|
|
|
|
let previousCode: String?
|
|
switch stateValue.with ({ $0.stage }) {
|
|
case let .code(text, _, _):
|
|
previousCode = text
|
|
default:
|
|
previousCode = nil
|
|
}
|
|
|
|
updateState { state in
|
|
switch state.stage {
|
|
case let .entry(text):
|
|
return state.withUpdatedStage(.entry(text: data[_id_input_entry_pwd]?.stringValue ?? text))
|
|
case let .reentry(first, text):
|
|
return state.withUpdatedStage(.reentry(first: first, text: data[_id_input_reentry_pwd]?.stringValue ?? text)).withUpdatedError(nil, for: _id_input_reentry_pwd)
|
|
case let .hint(password, text):
|
|
return state.withUpdatedStage(.hint(password: password, text: data[_id_input_entry_hint]?.stringValue ?? text)).withUpdatedError(nil, for: _id_input_entry_hint)
|
|
case let .email(password, hint, text, change):
|
|
return state.withUpdatedStage(.email(password: password, hint: hint, text: data[_id_input_entry_email]?.stringValue ?? text, change: change)).withUpdatedError(nil, for: _id_input_entry_email)
|
|
case let .code(text, codeLength, pattern):
|
|
return state.withUpdatedStage(.code(text: data[_id_input_entry_code]?.stringValue ?? text, codeLength: codeLength, pattern: pattern)).withUpdatedError(nil, for: _id_input_entry_code)
|
|
}
|
|
}
|
|
|
|
switch stateValue.with ({ $0.stage }) {
|
|
case let .code(text, codeLength, _):
|
|
if Int32(text.length) == codeLength, previousCode != text {
|
|
return checkAndSaveState(context: context)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
return .none
|
|
}, afterDisappear: {
|
|
actionsDisposable.dispose()
|
|
}, updateDoneValue: { data in
|
|
return { f in
|
|
updateState { state in
|
|
|
|
if state.updating {
|
|
f(.loading)
|
|
} else {
|
|
switch state.stage {
|
|
case let .entry(text):
|
|
if text.isEmpty {
|
|
f(.disabled(strings().navigationNext))
|
|
} else {
|
|
f(.enabled(strings().navigationNext))
|
|
}
|
|
case let .reentry(_, text):
|
|
if text.isEmpty {
|
|
f(.disabled(strings().navigationNext))
|
|
} else {
|
|
f(.enabled(strings().navigationNext))
|
|
}
|
|
case let .hint(_, text):
|
|
if text.isEmpty {
|
|
f(.enabled(strings().twoStepAuthEmailSkip))
|
|
} else {
|
|
f(.enabled(strings().navigationNext))
|
|
}
|
|
case let .email(_, _, text, _):
|
|
switch mode {
|
|
case .setupEmail:
|
|
f(text.isEmpty ? .disabled(strings().navigationNext) : .enabled(strings().navigationNext))
|
|
default:
|
|
f(text.isEmpty ? .enabled(strings().twoStepAuthEmailSkip) : .enabled(strings().navigationNext))
|
|
}
|
|
case let .code(text, codeLength, _):
|
|
if let codeLength = codeLength {
|
|
f(text.length < codeLength ? .disabled(strings().navigationNext) : .enabled(strings().navigationNext))
|
|
} else {
|
|
f(text.isEmpty ? .disabled(strings().navigationNext) : .enabled(strings().navigationNext))
|
|
}
|
|
}
|
|
}
|
|
return state
|
|
}
|
|
}
|
|
}, removeAfterDisappear: false, hasDone: true, identifier: "tsv-entry", afterTransaction: { controller in
|
|
var stage: PasswordEntryStage?
|
|
updateState { state in
|
|
stage = state.stage
|
|
return state
|
|
}
|
|
if let stage = stage {
|
|
var title: String = ""
|
|
|
|
switch stage {
|
|
case .entry:
|
|
switch mode {
|
|
case .change:
|
|
title = strings().twoStepAuthChangePassword
|
|
case .setup:
|
|
title = strings().twoStepAuthSetupPasswordTitle
|
|
case .setupEmail:
|
|
title = strings().twoStepAuthSetupPasswordTitle
|
|
case .enterCode:
|
|
preconditionFailure()
|
|
}
|
|
|
|
case .reentry:
|
|
switch mode {
|
|
case .change:
|
|
title = strings().twoStepAuthChangePassword
|
|
case .setup:
|
|
title = strings().twoStepAuthSetupPasswordTitle
|
|
case .setupEmail:
|
|
title = strings().twoStepAuthSetupPasswordTitle
|
|
case .enterCode:
|
|
preconditionFailure()
|
|
}
|
|
case .hint:
|
|
title = strings().twoStepAuthSetupHintTitle
|
|
case .email:
|
|
title = strings().twoStepAuthSetupEmailTitle
|
|
case .code:
|
|
title = strings().twoStepAuthSetupEmailTitle
|
|
}
|
|
controller.setCenterTitle(title)
|
|
}
|
|
})
|
|
|
|
}
|