import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SyncCore import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramStringFormatting import AccountContext import GalleryUI import CountrySelectionUI import DateSelectionUI import AppBundle private enum SecureIdDocumentFormTextField { case identifier case firstName case middleName case lastName case nativeFirstName case nativeMiddleName case nativeLastName case street1 case street2 case city case state case postcode } private enum SecureIdDocumentFormDateField { case birthdate case expiry } private enum SecureIdDocumentFormGenderField { case gender } private enum SecureIdDocumentFormSelectionField { case country case residenceCountry case date(Int32?, SecureIdDocumentFormDateField) case gender } enum SecureIdAddFileTarget { case scan case selfie case frontSide(SecureIdRequestedIdentityDocument?) case backSide(SecureIdRequestedIdentityDocument?) case translation } final class SecureIdDocumentFormParams { fileprivate let account: Account fileprivate let context: SecureIdAccessContext fileprivate let addFile: (SecureIdAddFileTarget) -> Void fileprivate let openDocument: (SecureIdVerificationDocument) -> Void fileprivate let deleteDocument: (SecureIdVerificationDocument) -> Void fileprivate let updateText: (SecureIdDocumentFormTextField, String) -> Void fileprivate let selectNextInputItem: (SecureIdDocumentFormEntry) -> Void fileprivate let endEditing: () -> Void fileprivate let activateSelection: (SecureIdDocumentFormSelectionField) -> Void fileprivate let scanPassport: () -> Void fileprivate let deleteValue: () -> Void fileprivate init(account: Account, context: SecureIdAccessContext, addFile: @escaping (SecureIdAddFileTarget) -> Void, openDocument: @escaping (SecureIdVerificationDocument) -> Void, deleteDocument: @escaping (SecureIdVerificationDocument) -> Void, updateText: @escaping (SecureIdDocumentFormTextField, String) -> Void, selectNextInputItem: @escaping (SecureIdDocumentFormEntry) -> Void, endEditing: @escaping () -> Void, activateSelection: @escaping (SecureIdDocumentFormSelectionField) -> Void, scanPassport: @escaping () -> Void, deleteValue: @escaping () -> Void) { self.account = account self.context = context self.addFile = addFile self.openDocument = openDocument self.deleteDocument = deleteDocument self.updateText = updateText self.selectNextInputItem = selectNextInputItem self.endEditing = endEditing self.activateSelection = activateSelection self.scanPassport = scanPassport self.deleteValue = deleteValue } } private struct SecureIdDocumentFormIdentityDetailsState: Equatable { let primaryLanguageByCountry: [String: String] let nativeNameRequired: Bool var firstName: String var middleName: String var lastName: String var nativeFirstName: String var nativeMiddleName: String var nativeLastName: String var countryCode: String var residenceCountryCode: String var birthdate: SecureIdDate? var gender: SecureIdGender? func isComplete() -> Bool { let nameMaxLength = 255 if self.firstName.isEmpty || self.firstName.count > nameMaxLength { return false } if self.middleName.count > nameMaxLength { return false } if self.lastName.isEmpty || self.lastName.count > nameMaxLength { return false } if self.nativeNameRequired && self.primaryLanguageByCountry[self.residenceCountryCode] != "en" { if self.nativeFirstName.isEmpty || self.nativeFirstName.count > nameMaxLength { return false } if self.nativeLastName.isEmpty || self.nativeLastName.count > nameMaxLength { return false } } if self.countryCode.isEmpty { return false } if self.residenceCountryCode.isEmpty { return false } if self.birthdate == nil { return false } if self.gender == nil { return false } return true } } enum DocumentExpirationDate: Equatable { case notSet case date(SecureIdDate) case doesNotExpire } private struct SecureIdDocumentFormIdentityDocumentState: Equatable { var type: SecureIdRequestedIdentityDocument var identifier: String var expiryDate: DocumentExpirationDate func isComplete() -> Bool { let identifierMaxLength = 24 if self.identifier.isEmpty || self.identifier.count > identifierMaxLength { return false } if case .notSet = expiryDate { return false } return true } } private struct SecureIdDocumentFormIdentityState { var details: SecureIdDocumentFormIdentityDetailsState? var document: SecureIdDocumentFormIdentityDocumentState? func isEqual(to: SecureIdDocumentFormIdentityState) -> Bool { if self.details != to.details { return false } if self.document != to.document { return false } return true } func isComplete() -> Bool { if let details = self.details { if !details.isComplete() { return false } } if let document = self.document { if !document.isComplete() { return false } } return true } } private struct SecureIdDocumentFormAddressDetailsState: Equatable { var street1: String var street2: String var city: String var state: String var countryCode: String var postcode: String func isComplete() -> Bool { let cityMinLength = 2 let stateMinLength = 2 let postcodeMaxLength = 12 if self.street1.isEmpty { return false } if self.city.count < cityMinLength { return false } if self.countryCode.isEmpty { return false } if self.countryCode == "US" && self.state.count < stateMinLength { return false } if self.postcode.isEmpty || self.postcode.count > postcodeMaxLength { return false } return true } } private struct SecureIdDocumentFormAddressState { var details: SecureIdDocumentFormAddressDetailsState? var document: SecureIdRequestedAddressDocument? func isEqual(to: SecureIdDocumentFormAddressState) -> Bool { if self.details != to.details { return false } if self.document != to.document { return false } return true } func isComplete() -> Bool { if let details = self.details { if !details.isComplete() { return false } } return true } } private enum SecureIdDocumentFormDocumentState { case identity(SecureIdDocumentFormIdentityState) case address(SecureIdDocumentFormAddressState) mutating func updateTextField(type: SecureIdDocumentFormTextField, value: String) { switch self { case var .identity(state): switch type { case .firstName: state.details?.firstName = value case .middleName: state.details?.middleName = value case .lastName: state.details?.lastName = value case .nativeFirstName: state.details?.nativeFirstName = value case .nativeMiddleName: state.details?.nativeMiddleName = value case .nativeLastName: state.details?.nativeLastName = value case .identifier: state.document?.identifier = value default: break } self = .identity(state) case var .address(state): switch type { case .street1: state.details?.street1 = value case .street2: state.details?.street2 = value case .city: state.details?.city = value case .state: state.details?.state = value case .postcode: state.details?.postcode = value default: break } self = .address(state) } } mutating func updateCountryCode(value: String) { switch self { case var .identity(state): state.details?.countryCode = value self = .identity(state) case var .address(state): state.details?.countryCode = value self = .address(state) } } mutating func updateResidenceCountryCode(value: String) { switch self { case var .identity(state): state.details?.residenceCountryCode = value self = .identity(state) case .address: break } } mutating func updateDateField(type: SecureIdDocumentFormDateField, value: SecureIdDate?) { switch self { case var .identity(state): switch type { case .birthdate: state.details?.birthdate = value case .expiry: if let value = value { state.document?.expiryDate = .date(value) } else { state.document?.expiryDate = .doesNotExpire } } self = .identity(state) case .address: break } } mutating func updateGenderField(type: SecureIdDocumentFormGenderField, value: SecureIdGender?) { switch self { case var .identity(state): switch type { case .gender: state.details?.gender = value } self = .identity(state) case .address: break } } func isEqual(to: SecureIdDocumentFormDocumentState) -> Bool { switch self { case let .identity(lhsValue): if case let .identity(rhsValue) = to, lhsValue.isEqual(to: rhsValue) { return true } else { return false } case let .address(lhsValue): if case let .address(rhsValue) = to, lhsValue.isEqual(to: rhsValue) { return true } else { return false } } } } extension SecureIdDocumentFormDocumentState { mutating func updateWithRecognizedData(_ data: SecureIdRecognizedDocumentData) { if case var .identity(state) = self { if var details = state.details { if details.firstName.isEmpty { details.firstName = data.firstName ?? "" } if details.lastName.isEmpty { details.lastName = data.lastName ?? "" } if details.birthdate == nil, let birthdate = data.birthDate { details.birthdate = SecureIdDate(timestamp: Int32(birthdate.timeIntervalSince1970)) } if details.gender == nil, let gender = data.gender { if gender == "M" { details.gender = .male } else { details.gender = .female } } if details.countryCode.isEmpty { details.countryCode = data.nationality ?? "" } if details.residenceCountryCode.isEmpty { details.residenceCountryCode = data.issuingCountry ?? "" } state.details = details } if var document = state.document { switch document.type { case .passport: break case .internalPassport: break case .driversLicense: break case .idCard: break } if document.identifier.isEmpty { document.identifier = data.documentNumber ?? "" } if document.expiryDate == .notSet { if let expiryDate = data.expiryDate { document.expiryDate = SecureIdDate(timestamp: Int32(expiryDate.timeIntervalSince1970)).flatMap(DocumentExpirationDate.date) ?? .notSet } else { document.expiryDate = .doesNotExpire } } state.document = document } self = .identity(state) } } } private enum SecureIdDocumentFormActionState { case none case saving case deleting } enum SecureIdDocumentFormInputState { case saveAvailable case saveNotAvailable case inProgress } private func maybeAddError(key: SecureIdValueContentErrorKey, value: SecureIdValueWithContext, entries: inout [FormControllerItemEntry], errorIndex: inout Int) { if let error = value.errors[key] { entries.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error, key))) errorIndex += 1 } } struct SecureIdDocumentFormState: FormControllerInnerState { fileprivate var previousValues: [SecureIdValueKey: SecureIdValueWithContext] fileprivate var documentState: SecureIdDocumentFormDocumentState fileprivate var documents: [SecureIdVerificationDocument] fileprivate var selfieRequired: Bool fileprivate var selfieDocument: SecureIdVerificationDocument? fileprivate var frontSideRequired: Bool fileprivate var frontSideDocument: SecureIdVerificationDocument? fileprivate var backSideRequired: Bool fileprivate var backSideDocument: SecureIdVerificationDocument? fileprivate var translationsRequired: Bool fileprivate var translations: [SecureIdVerificationDocument] fileprivate var actionState: SecureIdDocumentFormActionState fileprivate var requestOptionalData: Bool func isEqual(to: SecureIdDocumentFormState) -> Bool { if !self.documentState.isEqual(to: to.documentState) { return false } if self.actionState != to.actionState { return false } if self.documents.count != to.documents.count { return false } for i in 0 ..< self.documents.count { if self.documents[i] != to.documents[i] { return false } } if self.selfieRequired != to.selfieRequired { return false } if self.selfieDocument != to.selfieDocument { return false } if self.frontSideDocument != to.frontSideDocument { return false } if self.backSideDocument != to.backSideDocument { return false } if self.translationsRequired != to.translationsRequired { return false } if self.translations.count != to.translations.count { return false } for i in 0 ..< self.translations.count { if self.translations[i] != to.translations[i] { return false } } if self.requestOptionalData != to.requestOptionalData { return false } return true } func entries() -> [FormControllerItemEntry] { switch self.documentState { case let .identity(identity): var result: [FormControllerItemEntry] = [] var errorIndex = 0 if let details = identity.details { if identity.document == nil { result.append(.spacer) result.append(.entry(SecureIdDocumentFormEntry.scanYourPassport)) result.append(.entry(SecureIdDocumentFormEntry.scanYourPassportInfo)) result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) let previousValue: SecureIdValueWithContext? = self.previousValues[.personalDetails] let valueErrorKey: SecureIdValueContentErrorKey = .value(.personalDetails) if let previousValue = previousValue { maybeAddError(key: valueErrorKey, value: previousValue, entries: &result, errorIndex: &errorIndex) } result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstName))]))) result.append(.entry(SecureIdDocumentFormEntry.middleName(details.middleName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.middleName))]))) result.append(.entry(SecureIdDocumentFormEntry.lastName(details.lastName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.lastName))]))) result.append(.entry(SecureIdDocumentFormEntry.birthdate(details.birthdate, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.birthdate))]))) result.append(.entry(SecureIdDocumentFormEntry.gender(details.gender, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.gender))]))) result.append(.entry(SecureIdDocumentFormEntry.countryCode(.identity, details.countryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.residenceCountryCode(details.residenceCountryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.residenceCountryCode))]))) if (details.nativeNameRequired || self.requestOptionalData) && !details.residenceCountryCode.isEmpty && details.primaryLanguageByCountry[details.residenceCountryCode] != "en" { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.nativeInfoHeader(details.primaryLanguageByCountry[details.residenceCountryCode] ?? ""))) result.append(.entry(SecureIdDocumentFormEntry.nativeFirstName(details.nativeFirstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstNameNative))]))) result.append(.entry(SecureIdDocumentFormEntry.nativeMiddleName(details.nativeMiddleName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.middleNameNative))]))) result.append(.entry(SecureIdDocumentFormEntry.nativeLastName(details.nativeLastName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.lastNameNative))]))) result.append(.entry(SecureIdDocumentFormEntry.nativeInfo(details.primaryLanguageByCountry[details.residenceCountryCode] ?? "", details.residenceCountryCode))) result.append(.spacer) } } if let document = identity.document { if identity.details == nil { result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) } let previousValue: SecureIdValueWithContext? let valueErrorKey: SecureIdValueContentErrorKey var identifierError: String? var expiryDateError: String? switch document.type { case .passport: previousValue = self.previousValues[.passport] valueErrorKey = .value(.passport) identifierError = self.previousValues[.passport]?.errors[.field(.passport(.documentId))] expiryDateError = self.previousValues[.passport]?.errors[.field(.passport(.expiryDate))] case .internalPassport: previousValue = self.previousValues[.internalPassport] valueErrorKey = .value(.internalPassport) identifierError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.documentId))] expiryDateError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.expiryDate))] case .driversLicense: previousValue = self.previousValues[.driversLicense] valueErrorKey = .value(.driversLicense) identifierError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.documentId))] expiryDateError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.expiryDate))] case .idCard: previousValue = self.previousValues[.idCard] valueErrorKey = .value(.idCard) identifierError = self.previousValues[.idCard]?.errors[.field(.idCard(.documentId))] expiryDateError = self.previousValues[.idCard]?.errors[.field(.idCard(.expiryDate))] } if let previousValue = previousValue { maybeAddError(key: valueErrorKey, value: previousValue, entries: &result, errorIndex: &errorIndex) } result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier, identifierError))) result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate, expiryDateError))) } if ((self.selfieRequired || self.requestOptionalData) && identity.document != nil) || self.frontSideRequired || self.backSideRequired { let type = identity.document?.type if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.requestedDocumentsHeader)) if self.frontSideRequired { if let document = self.frontSideDocument { var error: String? if case let .remote(file) = document { switch self.documentState { case let .identity(identity): if let document = identity.document { switch document.type { case .passport: error = self.previousValues[.passport]?.errors[.frontSide(hash: file.fileHash)] case .internalPassport: error = self.previousValues[.internalPassport]?.errors[.frontSide(hash: file.fileHash)] case .driversLicense: error = self.previousValues[.driversLicense]?.errors[.frontSide(hash: file.fileHash)] case .idCard: error = self.previousValues[.idCard]?.errors[.frontSide(hash: file.fileHash)] } } case .address: break } } result.append(.entry(SecureIdDocumentFormEntry.frontSide(1, type, document, error))) } else { result.append(.entry(SecureIdDocumentFormEntry.frontSide(1, type, nil, nil))) } } if self.backSideRequired { if let document = self.backSideDocument { var error: String? if case let .remote(file) = document { switch self.documentState { case let .identity(identity): if let document = identity.document { switch document.type { case .passport: error = self.previousValues[.passport]?.errors[.backSide(hash: file.fileHash)] case .internalPassport: error = self.previousValues[.internalPassport]?.errors[.backSide(hash: file.fileHash)] case .driversLicense: error = self.previousValues[.driversLicense]?.errors[.backSide(hash: file.fileHash)] case .idCard: error = self.previousValues[.idCard]?.errors[.backSide(hash: file.fileHash)] } } case .address: break } } result.append(.entry(SecureIdDocumentFormEntry.backSide(2, type, document, error))) } else { result.append(.entry(SecureIdDocumentFormEntry.backSide(2, type, nil, nil))) } } if self.selfieRequired || self.requestOptionalData { if let document = self.selfieDocument { var error: String? if case let .remote(file) = document { switch self.documentState { case let .identity(identity): if let document = identity.document { switch document.type { case .passport: error = self.previousValues[.passport]?.errors[.selfie(hash: file.fileHash)] case .internalPassport: error = self.previousValues[.internalPassport]?.errors[.selfie(hash: file.fileHash)] case .driversLicense: error = self.previousValues[.driversLicense]?.errors[.selfie(hash: file.fileHash)] case .idCard: error = self.previousValues[.idCard]?.errors[.selfie(hash: file.fileHash)] } } case .address: break } } result.append(.entry(SecureIdDocumentFormEntry.selfie(0, document, error))) } else { result.append(.entry(SecureIdDocumentFormEntry.selfie(0, nil, nil))) } } result.append(.entry(SecureIdDocumentFormEntry.scansInfo(.identity))) } if let document = identity.document, self.translationsRequired || self.requestOptionalData { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.translationsHeader)) let filesType: SecureIdValueKey switch document.type { case .passport: filesType = .passport case .internalPassport: filesType = .internalPassport case .driversLicense: filesType = .driversLicense case .idCard: filesType = .idCard } if let value = self.previousValues[filesType] { var fileHashes: Set? = Set() loop: for document in self.translations { switch document { case .local: fileHashes = nil break loop case let .remote(file): fileHashes?.insert(file.fileHash) } } if let fileHashes = fileHashes, !fileHashes.isEmpty { maybeAddError(key: .translationFiles(hashes: fileHashes), value: value, entries: &result, errorIndex: &errorIndex) } } for i in 0 ..< self.translations.count { var error: String? switch self.translations[i] { case .local: break case let .remote(file): switch self.documentState { case let .identity(identity): if let document = identity.document { switch document.type { case .passport: error = self.previousValues[.passport]?.errors[.translationFile(hash: file.fileHash)] case .internalPassport: error = self.previousValues[.internalPassport]?.errors[.translationFile(hash: file.fileHash)] case .driversLicense: error = self.previousValues[.driversLicense]?.errors[.translationFile(hash: file.fileHash)] case .idCard: error = self.previousValues[.idCard]?.errors[.translationFile(hash: file.fileHash)] } } case let .address(address): if let document = address.document { switch document { case .passportRegistration: error = self.previousValues[.passportRegistration]?.errors[.translationFile(hash: file.fileHash)] case .temporaryRegistration: error = self.previousValues[.temporaryRegistration]?.errors[.translationFile(hash: file.fileHash)] case .bankStatement: error = self.previousValues[.bankStatement]?.errors[.translationFile(hash: file.fileHash)] case .utilityBill: error = self.previousValues[.utilityBill]?.errors[.translationFile(hash: file.fileHash)] case .rentalAgreement: error = self.previousValues[.rentalAgreement]?.errors[.translationFile(hash: file.fileHash)] } } } } result.append(.entry(SecureIdDocumentFormEntry.translation(i, self.translations[i], error))) } result.append(.entry(SecureIdDocumentFormEntry.addTranslation(!self.translations.isEmpty))) result.append(.entry(SecureIdDocumentFormEntry.translationsInfo)) result.append(.spacer) } if !self.previousValues.isEmpty { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.deleteDocument(.identity, identity.document != nil))) } return result case let .address(address): var result: [FormControllerItemEntry] = [] var errorIndex = 0 if let details = address.details { result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.address))) let previousValue: SecureIdValueWithContext? = self.previousValues[.address] let valueErrorKey: SecureIdValueContentErrorKey = .value(.address) if let previousValue = previousValue { maybeAddError(key: valueErrorKey, value: previousValue, entries: &result, errorIndex: &errorIndex) } result.append(.entry(SecureIdDocumentFormEntry.street1(details.street1, self.previousValues[.address]?.errors[.field(.address(.streetLine1))]))) result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2, self.previousValues[.address]?.errors[.field(.address(.streetLine2))]))) result.append(.entry(SecureIdDocumentFormEntry.city(details.city, self.previousValues[.address]?.errors[.field(.address(.city))]))) result.append(.entry(SecureIdDocumentFormEntry.state(details.state, self.previousValues[.address]?.errors[.field(.address(.state))]))) result.append(.entry(SecureIdDocumentFormEntry.countryCode(.address, details.countryCode, self.previousValues[.address]?.errors[.field(.address(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode, self.previousValues[.address]?.errors[.field(.address(.postCode))]))) } if let document = address.document { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.scansHeader)) let filesType: SecureIdValueKey switch document { case .passportRegistration: filesType = .passportRegistration case .temporaryRegistration: filesType = .temporaryRegistration case .bankStatement: filesType = .bankStatement case .rentalAgreement: filesType = .rentalAgreement case .utilityBill: filesType = .utilityBill } if let value = self.previousValues[filesType] { var fileHashes: Set? = Set() loop: for document in self.documents { switch document { case .local: fileHashes = nil break loop case let .remote(file): fileHashes?.insert(file.fileHash) } } if let fileHashes = fileHashes, !fileHashes.isEmpty { maybeAddError(key: .files(hashes: fileHashes), value: value, entries: &result, errorIndex: &errorIndex) } } for i in 0 ..< self.documents.count { var error: String? switch self.documents[i] { case .local: break case let .remote(file): switch self.documentState { case let .identity(identity): if let document = identity.document { switch document.type { case .passport: error = self.previousValues[.passport]?.errors[.file(hash: file.fileHash)] case .internalPassport: error = self.previousValues[.internalPassport]?.errors[.file(hash: file.fileHash)] case .driversLicense: error = self.previousValues[.driversLicense]?.errors[.file(hash: file.fileHash)] case .idCard: error = self.previousValues[.idCard]?.errors[.file(hash: file.fileHash)] } } case let .address(address): if let document = address.document { switch document { case .passportRegistration: error = self.previousValues[.passportRegistration]?.errors[.file(hash: file.fileHash)] case .temporaryRegistration: error = self.previousValues[.temporaryRegistration]?.errors[.file(hash: file.fileHash)] case .bankStatement: error = self.previousValues[.bankStatement]?.errors[.file(hash: file.fileHash)] case .utilityBill: error = self.previousValues[.utilityBill]?.errors[.file(hash: file.fileHash)] case .rentalAgreement: error = self.previousValues[.rentalAgreement]?.errors[.file(hash: file.fileHash)] } } } } result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i], error))) } result.append(.entry(SecureIdDocumentFormEntry.addScan(!self.documents.isEmpty))) result.append(.entry(SecureIdDocumentFormEntry.scansInfo(.address))) result.append(.spacer) } if let document = address.document, self.translationsRequired || self.requestOptionalData { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.translationsHeader)) let filesType: SecureIdValueKey switch document { case .passportRegistration: filesType = .passportRegistration case .temporaryRegistration: filesType = .temporaryRegistration case .bankStatement: filesType = .bankStatement case .rentalAgreement: filesType = .rentalAgreement case .utilityBill: filesType = .utilityBill } if let value = self.previousValues[filesType] { var fileHashes: Set? = Set() loop: for document in self.translations { switch document { case .local: fileHashes = nil break loop case let .remote(file): fileHashes?.insert(file.fileHash) } } if let fileHashes = fileHashes, !fileHashes.isEmpty { maybeAddError(key: .translationFiles(hashes: fileHashes), value: value, entries: &result, errorIndex: &errorIndex) } } for i in 0 ..< self.translations.count { var error: String? switch self.translations[i] { case .local: break case let .remote(file): switch self.documentState { case let .address(address): if let document = address.document { switch document { case .passportRegistration: error = self.previousValues[.passportRegistration]?.errors[.translationFile(hash: file.fileHash)] case .temporaryRegistration: error = self.previousValues[.temporaryRegistration]?.errors[.translationFile(hash: file.fileHash)] case .bankStatement: error = self.previousValues[.bankStatement]?.errors[.translationFile(hash: file.fileHash)] case .utilityBill: error = self.previousValues[.utilityBill]?.errors[.translationFile(hash: file.fileHash)] case .rentalAgreement: error = self.previousValues[.rentalAgreement]?.errors[.translationFile(hash: file.fileHash)] } } default: break } } result.append(.entry(SecureIdDocumentFormEntry.translation(i, self.translations[i], error))) } result.append(.entry(SecureIdDocumentFormEntry.addTranslation(!self.translations.isEmpty))) result.append(.entry(SecureIdDocumentFormEntry.translationsInfo)) result.append(.spacer) } if !self.previousValues.isEmpty { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } result.append(.entry(SecureIdDocumentFormEntry.deleteDocument(.address, address.document != nil))) } return result } } func actionInputState() -> SecureIdDocumentFormInputState { switch self.actionState { case .deleting, .saving: return .inProgress default: break } var badHashes: Set = [] var badFileHashes: Set? var badTranslationHashes: Set? for value in self.previousValues { for error in value.value.errors { switch error.key { case .value(value.key), .field: return .saveNotAvailable case let .file(hash), let .selfie(hash), let .frontSide(hash), let .backSide(hash), let .translationFile(hash): badHashes.insert(hash) case let .files(hashes): badFileHashes = hashes case let .translationFiles(hashes): badTranslationHashes = hashes default: break } } } var documentsRequired = false switch self.documentState { case let .identity(identity): if !identity.isComplete() { return .saveNotAvailable } case let .address(address): if !address.isComplete() { return .saveNotAvailable } if address.document != nil { documentsRequired = true } } func isDocumentReady(_ document: SecureIdVerificationDocument?, badHashes: Set? = nil) -> Bool { if let document = document { switch document { case let .local(local): switch local.state { case .uploading: return false case .uploaded: return true } case let .remote(reference): if let badHashes = badHashes { return !badHashes.contains(reference.fileHash) } else { return true } } } else { return false } } if self.frontSideRequired { guard isDocumentReady(self.frontSideDocument, badHashes: badHashes) else { return .saveNotAvailable } } if self.backSideRequired { guard isDocumentReady(self.backSideDocument, badHashes: badHashes) else { return .saveNotAvailable } } if self.selfieRequired { guard isDocumentReady(self.selfieDocument, badHashes: badHashes) else { return .saveNotAvailable } } var fileHashes: Set = [] for document in self.documents { guard isDocumentReady(document) else { return .saveNotAvailable } switch document { case let .remote(reference): fileHashes.insert(reference.fileHash) case let .local(document): if case let .uploaded(file) = document.state { fileHashes.insert(file.fileHash) } } } if documentsRequired && self.documents.isEmpty { return .saveNotAvailable } if let badFileHashes = badFileHashes, badFileHashes == fileHashes { return .saveNotAvailable } var translationHashes: Set = [] for document in self.translations { guard isDocumentReady(document) else { return .saveNotAvailable } switch document { case let .remote(reference): translationHashes.insert(reference.fileHash) case let .local(document): if case let .uploaded(file) = document.state { translationHashes.insert(file.fileHash) } } } if self.translationsRequired && self.translations.isEmpty { return .saveNotAvailable } if let badTranslationHashes = badTranslationHashes, badTranslationHashes == translationHashes { return .saveNotAvailable } return .saveAvailable } } extension SecureIdDocumentFormState { init(requestedData: SecureIdDocumentFormRequestedData, values: [SecureIdValueKey: SecureIdValueWithContext], requestOptionalData: Bool, primaryLanguageByCountry: [String: String]) { switch requestedData { case let .identity(details, document, selfie, translations): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] var detailsState: SecureIdDocumentFormIdentityDetailsState? if let details = details { if let value = values[.personalDetails], case let .personalDetails(personalDetailsValue) = value.value { previousValues[.personalDetails] = value detailsState = SecureIdDocumentFormIdentityDetailsState(primaryLanguageByCountry: primaryLanguageByCountry, nativeNameRequired: details.nativeNames, firstName: personalDetailsValue.latinName.firstName, middleName: personalDetailsValue.latinName.middleName, lastName: personalDetailsValue.latinName.lastName, nativeFirstName: personalDetailsValue.nativeName?.firstName ?? "", nativeMiddleName: personalDetailsValue.nativeName?.middleName ?? "", nativeLastName: personalDetailsValue.nativeName?.lastName ?? "", countryCode: personalDetailsValue.countryCode, residenceCountryCode: personalDetailsValue.residenceCountryCode, birthdate: personalDetailsValue.birthdate, gender: personalDetailsValue.gender) } else { detailsState = SecureIdDocumentFormIdentityDetailsState(primaryLanguageByCountry: primaryLanguageByCountry, nativeNameRequired: details.nativeNames, firstName: "", middleName: "", lastName: "", nativeFirstName: "", nativeMiddleName: "", nativeLastName: "", countryCode: "", residenceCountryCode: "", birthdate: nil, gender: nil) } } var documentState: SecureIdDocumentFormIdentityDocumentState? var verificationDocuments: [SecureIdVerificationDocument] = [] var selfieDocument: SecureIdVerificationDocument? var frontSideRequired: Bool = false var backSideRequired: Bool = false var frontSideDocument: SecureIdVerificationDocument? var backSideDocument: SecureIdVerificationDocument? var translationDocuments: [SecureIdVerificationDocument] = [] if let document = document { var identifier: String = "" var expiryDate: DocumentExpirationDate = .notSet switch document { case .passport: if let value = values[.passport], case let .passport(passport) = value.value { previousValues[value.value.key] = value identifier = passport.identifier expiryDate = passport.expiryDate.flatMap(DocumentExpirationDate.date) ?? .doesNotExpire verificationDocuments = passport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) frontSideDocument = passport.frontSideDocument.flatMap(SecureIdVerificationDocument.init) selfieDocument = passport.selfieDocument.flatMap(SecureIdVerificationDocument.init) translationDocuments = passport.translations.compactMap(SecureIdVerificationDocument.init) } frontSideRequired = true case .internalPassport: if let value = values[.internalPassport], case let .internalPassport(internalPassport) = value.value { previousValues[value.value.key] = value identifier = internalPassport.identifier expiryDate = internalPassport.expiryDate.flatMap(DocumentExpirationDate.date) ?? .doesNotExpire verificationDocuments = internalPassport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = internalPassport.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = internalPassport.frontSideDocument.flatMap(SecureIdVerificationDocument.init) translationDocuments = internalPassport.translations.compactMap(SecureIdVerificationDocument.init) } frontSideRequired = true case .driversLicense: if let value = values[.driversLicense], case let .driversLicense(driversLicense) = value.value { previousValues[value.value.key] = value identifier = driversLicense.identifier expiryDate = driversLicense.expiryDate.flatMap(DocumentExpirationDate.date) ?? .doesNotExpire verificationDocuments = driversLicense.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = driversLicense.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = driversLicense.frontSideDocument.flatMap(SecureIdVerificationDocument.init) backSideDocument = driversLicense.backSideDocument.flatMap(SecureIdVerificationDocument.init) translationDocuments = driversLicense.translations.compactMap(SecureIdVerificationDocument.init) } frontSideRequired = true backSideRequired = true case .idCard: if let value = values[.idCard], case let .idCard(idCard) = value.value { previousValues[value.value.key] = value identifier = idCard.identifier expiryDate = idCard.expiryDate.flatMap(DocumentExpirationDate.date) ?? .doesNotExpire verificationDocuments = idCard.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = idCard.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = idCard.frontSideDocument.flatMap(SecureIdVerificationDocument.init) backSideDocument = idCard.backSideDocument.flatMap(SecureIdVerificationDocument.init) translationDocuments = idCard.translations.compactMap(SecureIdVerificationDocument.init) } frontSideRequired = true backSideRequired = true } documentState = SecureIdDocumentFormIdentityDocumentState(type: document, identifier: identifier, expiryDate: expiryDate) } let formState = SecureIdDocumentFormIdentityState(details: detailsState, document: documentState) self.init(previousValues: previousValues, documentState: .identity(formState), documents: verificationDocuments, selfieRequired: selfie, selfieDocument: selfieDocument, frontSideRequired: frontSideRequired, frontSideDocument: frontSideDocument, backSideRequired: backSideRequired, backSideDocument: backSideDocument, translationsRequired: translations, translations: translationDocuments, actionState: .none, requestOptionalData: requestOptionalData) case let .address(details, document, translations): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] var detailsState: SecureIdDocumentFormAddressDetailsState? var documentState: SecureIdRequestedAddressDocument? var verificationDocuments: [SecureIdVerificationDocument] = [] var translationDocuments: [SecureIdVerificationDocument] = [] if details { if let value = values[.address], case let .address(address) = value.value { previousValues[value.value.key] = value detailsState = SecureIdDocumentFormAddressDetailsState(street1: address.street1, street2: address.street2, city: address.city, state: address.state, countryCode: address.countryCode, postcode: address.postcode) } else { detailsState = SecureIdDocumentFormAddressDetailsState(street1: "", street2: "", city: "", state: "", countryCode: "", postcode: "") } } if let document = document { switch document { case .passportRegistration: if let value = values[.passportRegistration], case let .passportRegistration(passportRegistration) = value.value { previousValues[value.value.key] = value verificationDocuments = passportRegistration.verificationDocuments.compactMap(SecureIdVerificationDocument.init) translationDocuments = passportRegistration.translations.compactMap(SecureIdVerificationDocument.init) } case .temporaryRegistration: if let value = values[.temporaryRegistration], case let .temporaryRegistration(temporaryRegistration) = value.value { previousValues[value.value.key] = value verificationDocuments = temporaryRegistration.verificationDocuments.compactMap(SecureIdVerificationDocument.init) translationDocuments = temporaryRegistration.translations.compactMap(SecureIdVerificationDocument.init) } case .bankStatement: if let value = values[.bankStatement], case let .bankStatement(bankStatement) = value.value { previousValues[value.value.key] = value verificationDocuments = bankStatement.verificationDocuments.compactMap(SecureIdVerificationDocument.init) translationDocuments = bankStatement.translations.compactMap(SecureIdVerificationDocument.init) } case .utilityBill: if let value = values[.utilityBill], case let .utilityBill(utilityBill) = value.value { previousValues[value.value.key] = value verificationDocuments = utilityBill.verificationDocuments.compactMap(SecureIdVerificationDocument.init) translationDocuments = utilityBill.translations.compactMap(SecureIdVerificationDocument.init) } case .rentalAgreement: if let value = values[.rentalAgreement], case let .rentalAgreement(rentalAgreement) = value.value { previousValues[value.value.key] = value verificationDocuments = rentalAgreement.verificationDocuments.compactMap(SecureIdVerificationDocument.init) translationDocuments = rentalAgreement.translations.compactMap(SecureIdVerificationDocument.init) } } documentState = document } let formState = SecureIdDocumentFormAddressState(details: detailsState, document: documentState) self.init(previousValues: previousValues, documentState: .address(formState), documents: verificationDocuments, selfieRequired: false, selfieDocument: nil, frontSideRequired: false, frontSideDocument: nil, backSideRequired: false, backSideDocument: nil, translationsRequired: translations, translations: translationDocuments, actionState: .none, requestOptionalData: requestOptionalData) } } func makeValues() -> [SecureIdValueKey: SecureIdValue]? { var verificationDocuments: [SecureIdVerificationDocumentReference] = [] for document in self.documents { switch document { case let .remote(file): verificationDocuments.append(.remote(file)) case let .local(file): switch file.state { case let .uploaded(file): verificationDocuments.append(.uploaded(file)) case .uploading: return nil } } } var selfieDocument: SecureIdVerificationDocumentReference? if let document = self.selfieDocument { switch document { case let .remote(file): selfieDocument = .remote(file) case let .local(file): switch file.state { case let .uploaded(file): selfieDocument = .uploaded(file) case .uploading: return nil } } } var frontSideDocument: SecureIdVerificationDocumentReference? if let document = self.frontSideDocument { switch document { case let .remote(file): frontSideDocument = .remote(file) case let .local(file): switch file.state { case let .uploaded(file): frontSideDocument = .uploaded(file) case .uploading: return nil } } } var backSideDocument: SecureIdVerificationDocumentReference? if let document = self.backSideDocument { switch document { case let .remote(file): backSideDocument = .remote(file) case let .local(file): switch file.state { case let .uploaded(file): backSideDocument = .uploaded(file) case .uploading: return nil } } } var translationDocuments: [SecureIdVerificationDocumentReference] = [] for document in self.translations { switch document { case let .remote(file): translationDocuments.append(.remote(file)) case let .local(file): switch file.state { case let .uploaded(file): translationDocuments.append(.uploaded(file)) case .uploading: return nil } } } switch self.documentState { case let .identity(identity): var values: [SecureIdValueKey: SecureIdValue] = [:] if let details = identity.details { guard !details.firstName.isEmpty else { return nil } guard !details.lastName.isEmpty else { return nil } guard !details.countryCode.isEmpty else { return nil } guard !details.residenceCountryCode.isEmpty else { return nil } guard let birthdate = details.birthdate else { return nil } guard let gender = details.gender else { return nil } values[.personalDetails] = .personalDetails(SecureIdPersonalDetailsValue(latinName: SecureIdPersonName(firstName: details.firstName, lastName: details.lastName, middleName: details.middleName), nativeName: SecureIdPersonName(firstName: details.nativeFirstName, lastName: details.nativeLastName, middleName: details.nativeMiddleName), birthdate: birthdate, countryCode: details.countryCode, residenceCountryCode: details.residenceCountryCode, gender: gender)) } if let document = identity.document { guard !document.identifier.isEmpty else { return nil } let expirationDate: SecureIdDate? switch document.expiryDate { case .notSet: return nil case .doesNotExpire: expirationDate = nil case let .date(value): expirationDate = value } switch document.type { case .passport: values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, expiryDate: expirationDate, verificationDocuments: verificationDocuments, translations: translationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) case .internalPassport: values[.internalPassport] = .internalPassport(SecureIdInternalPassportValue(identifier: document.identifier, expiryDate: expirationDate, verificationDocuments: verificationDocuments, translations: translationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) case .driversLicense: values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: expirationDate, verificationDocuments: verificationDocuments, translations: translationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument, backSideDocument: backSideDocument)) case .idCard: values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: expirationDate, verificationDocuments: verificationDocuments, translations: translationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument, backSideDocument: backSideDocument)) } } return values case let .address(address): var values: [SecureIdValueKey: SecureIdValue] = [:] if let details = address.details { guard !details.street1.isEmpty else { return nil } guard !details.city.isEmpty else { return nil } guard !details.countryCode.isEmpty else { return nil } guard !details.postcode.isEmpty else { return nil } values[.address] = .address(SecureIdAddressValue(street1: details.street1, street2: details.street2, city: details.city, state: details.state, countryCode: details.countryCode, postcode: details.postcode)) } if let document = address.document { guard !verificationDocuments.isEmpty else { return nil } switch document { case .passportRegistration: values[.passportRegistration] = .passportRegistration(SecureIdPassportRegistrationValue(verificationDocuments: verificationDocuments, translations: translationDocuments)) case .temporaryRegistration: values[.temporaryRegistration] = .temporaryRegistration(SecureIdTemporaryRegistrationValue(verificationDocuments: verificationDocuments, translations: translationDocuments)) case .bankStatement: values[.bankStatement] = .bankStatement(SecureIdBankStatementValue(verificationDocuments: verificationDocuments, translations: translationDocuments)) case .utilityBill: values[.utilityBill] = .utilityBill(SecureIdUtilityBillValue(verificationDocuments: verificationDocuments, translations: translationDocuments)) case .rentalAgreement: values[.rentalAgreement] = .rentalAgreement(SecureIdRentalAgreementValue(verificationDocuments: verificationDocuments, translations: translationDocuments)) } } return values } } } private func removeDocumentWithId(_ innerState: SecureIdDocumentFormState, id: SecureIdVerificationDocumentId) -> SecureIdDocumentFormState { var innerState = innerState if let selfieDocument = innerState.selfieDocument, selfieDocument.id == id { innerState.selfieDocument = nil } if let frontSideDocument = innerState.frontSideDocument, frontSideDocument.id == id { innerState.frontSideDocument = nil } if let backSideDocument = innerState.backSideDocument, backSideDocument.id == id { innerState.backSideDocument = nil } for i in 0 ..< innerState.documents.count { if innerState.documents[i].id == id { innerState.documents.remove(at: i) break } } for i in 0 ..< innerState.translations.count { if innerState.translations[i].id == id { innerState.translations.remove(at: i) break } } return innerState } enum SecureIdDocumentFormEntryId: Hashable { case scanYourPassport case scanYourPassportInfo case scansHeader case scan(SecureIdVerificationDocumentId) case addScan case scansInfo case infoHeader case identifier case firstName case middleName case lastName case nativeInfoHeader case nativeFirstName case nativeMiddleName case nativeLastName case nativeInfo case gender case countryCode case residenceCountryCode case birthdate case expiryDate case deleteDocument case requestedDocumentsHeader case selfie case frontSide case backSide case documentsInfo case translationsHeader case translation(SecureIdVerificationDocumentId) case addTranslation case translationsInfo case street1 case street2 case city case state case postcode case error(SecureIdValueContentErrorKey) } enum SecureIdDocumentFormEntryCategory { case identity case address } enum SecureIdDocumentFormEntry: FormControllerEntry { case scanYourPassport case scanYourPassportInfo case scansHeader case scan(Int, SecureIdVerificationDocument, String?) case addScan(Bool) case scansInfo(SecureIdDocumentFormEntryCategory) case infoHeader(SecureIdDocumentFormEntryCategory) case identifier(String, String?) case firstName(String, String?) case middleName(String, String?) case lastName(String, String?) case nativeInfoHeader(String) case nativeFirstName(String, String?) case nativeMiddleName(String, String?) case nativeLastName(String, String?) case nativeInfo(String, String) case gender(SecureIdGender?, String?) case countryCode(SecureIdDocumentFormEntryCategory, String, String?) case residenceCountryCode(String, String?) case birthdate(SecureIdDate?, String?) case expiryDate(DocumentExpirationDate, String?) case deleteDocument(SecureIdDocumentFormEntryCategory, Bool) case requestedDocumentsHeader case selfie(Int, SecureIdVerificationDocument?, String?) case frontSide(Int, SecureIdRequestedIdentityDocument?, SecureIdVerificationDocument?, String?) case backSide(Int, SecureIdRequestedIdentityDocument?, SecureIdVerificationDocument?, String?) case documentsInfo(SecureIdDocumentFormEntryCategory) case translationsHeader case translation(Int, SecureIdVerificationDocument, String?) case addTranslation(Bool) case translationsInfo case error(Int, String, SecureIdValueContentErrorKey) case street1(String, String?) case street2(String, String?) case city(String, String?) case state(String, String?) case postcode(String, String?) var stableId: SecureIdDocumentFormEntryId { switch self { case .scanYourPassport: return .scanYourPassport case .scanYourPassportInfo: return .scanYourPassportInfo case .scansHeader: return .scansHeader case let .scan(_, document, _): return .scan(document.id) case .addScan: return .addScan case .scansInfo: return .scansInfo case .infoHeader: return .infoHeader case .identifier: return .identifier case .firstName: return .firstName case .middleName: return .middleName case .lastName: return .lastName case .nativeInfoHeader: return .nativeInfoHeader case .nativeFirstName: return .nativeFirstName case .nativeMiddleName: return .nativeMiddleName case .nativeLastName: return .nativeLastName case .nativeInfo: return .nativeInfo case .countryCode: return .countryCode case .residenceCountryCode: return .residenceCountryCode case .birthdate: return .birthdate case .expiryDate: return .expiryDate case .deleteDocument: return .deleteDocument case .street1: return .street1 case .street2: return .street2 case .city: return .city case .state: return .state case .postcode: return .postcode case .gender: return .gender case .requestedDocumentsHeader: return .requestedDocumentsHeader case .selfie: return .selfie case .frontSide: return .frontSide case .backSide: return .backSide case .documentsInfo: return .documentsInfo case .translationsHeader: return .translationsHeader case let .translation(_, document, _): return .translation(document.id) case .addTranslation: return .addTranslation case .translationsInfo: return .translationsInfo case let .error(_, _, key): return .error(key) } } func isEqual(to: SecureIdDocumentFormEntry) -> Bool { switch self { case .scanYourPassport: if case .scanYourPassport = to { return true } else { return false } case .scanYourPassportInfo: if case .scanYourPassportInfo = to { return true } else { return false } case .scansHeader: if case .scansHeader = to { return true } else { return false } case let .scan(lhsId, lhsDocument, lhsError): if case let .scan(rhsId, rhsDocument, rhsError) = to, lhsId == rhsId, lhsDocument == rhsDocument, lhsError == rhsError { return true } else { return false } case let .addScan(hasAny): if case .addScan(hasAny) = to { return true } else { return false } case let .scansInfo(value): if case .scansInfo(value) = to { return true } else { return false } case let .infoHeader(value): if case .infoHeader(value) = to { return true } else { return false } case let .identifier(value, error): if case .identifier(value, error) = to { return true } else { return false } case let .firstName(value, error): if case .firstName(value, error) = to { return true } else { return false } case let .middleName(value, error): if case .middleName(value, error) = to { return true } else { return false } case let .lastName(value, error): if case .lastName(value, error) = to { return true } else { return false } case let .nativeInfoHeader(language): if case .nativeInfoHeader(language) = to { return true } else { return false } case let .nativeFirstName(value, error): if case .nativeFirstName(value, error) = to { return true } else { return false } case let .nativeMiddleName(value, error): if case .nativeMiddleName(value, error) = to { return true } else { return false } case let .nativeLastName(value, error): if case .nativeLastName(value, error) = to { return true } else { return false } case let .nativeInfo(language, countryCode): if case .nativeInfo(language, countryCode) = to { return true } else { return false } case let .gender(value, error): if case .gender(value, error) = to { return true } else { return false } case let .countryCode(category, value, error): if case .countryCode(category, value, error) = to { return true } else { return false } case let .residenceCountryCode(value, error): if case .residenceCountryCode(value, error) = to { return true } else { return false } case let .birthdate(lhsValue, lhsError): if case let .birthdate(rhsValue, rhsError) = to, lhsValue == rhsValue, lhsError == rhsError { return true } else { return false } case let .expiryDate(lhsValue, lhsError): if case let .expiryDate(rhsValue, rhsError) = to, lhsValue == rhsValue, lhsError == rhsError { return true } else { return false } case let .deleteDocument(lhsCategory, lhsHasDocument): if case let .deleteDocument(rhsCategory, rhsHasDocument) = to, lhsCategory == rhsCategory, lhsHasDocument == rhsHasDocument { return true } else { return false } case let .street1(value, error): if case .street1(value, error) = to { return true } else { return false } case let .street2(value, error): if case .street2(value, error) = to { return true } else { return false } case let .city(value, error): if case .city(value, error) = to { return true } else { return false } case let .state(value, error): if case .state(value, error) = to { return true } else { return false } case let .postcode(value, error): if case .postcode(value, error) = to { return true } else { return false } case .requestedDocumentsHeader: if case .requestedDocumentsHeader = to { return true } else { return false } case let .selfie(index, document, error): if case .selfie(index, document, error) = to { return true } else { return false } case let .frontSide(index, type, document, error): if case .frontSide(index, type, document, error) = to { return true } else { return false } case let .backSide(index, type, document, error): if case .backSide(index, type, document, error) = to { return true } else { return false } case let .documentsInfo(category): if case .documentsInfo(category) = to { return true } else { return false } case .translationsHeader: if case .translationsHeader = to { return true } else { return false } case let .translation(index, document, error): if case .translation(index, document, error) = to { return true } else { return false } case let .addTranslation(hasAny): if case .addTranslation(hasAny) = to { return true } else{ return false } case .translationsInfo: if case .translationsInfo = to { return true } else { return false } case let .error(index, text, key): if case .error(index, text, key) = to { return true } else { return false } } } func item(params: SecureIdDocumentFormParams, strings: PresentationStrings) -> FormControllerItem { switch self { case .scanYourPassport: return FormControllerActionItem(type: .accent, title: strings.Passport_ScanPassport, activated: { params.scanPassport() }) case .scanYourPassportInfo: return FormControllerTextItem(text: strings.Passport_ScanPassportHelp) case .scansHeader: return FormControllerHeaderItem(text: strings.Passport_Scans) case let .scan(index, document, error): return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: nil, title: strings.Passport_Scans_ScanIndex("\(index + 1)").0, label: error.flatMap(SecureIdValueFormFileItemLabel.error) ?? .timestamp, activated: { params.openDocument(document) }, deleted: { params.deleteDocument(document) }) case let .addScan(hasAny): return FormControllerActionItem(type: .accent, title: hasAny ? strings.Passport_Scans_UploadNew : strings.Passport_Scans_Upload, fullTopInset: true, activated: { params.addFile(.scan) }) case let .scansInfo(type): let text: String switch type { case .identity: text = strings.Passport_Identity_ScansHelp case .address: text = strings.Passport_Address_ScansHelp } return FormControllerTextItem(text: text) case let .infoHeader(type): let text: String switch type { case .identity: text = strings.Passport_Identity_DocumentDetails case .address: text = strings.Passport_Address_Address } return FormControllerHeaderItem(text: text) case let .identifier(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_DocumentNumber, text: value, placeholder: strings.Passport_Identity_DocumentNumberPlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.identifier, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .firstName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_Name, text: value, placeholder: strings.Passport_Identity_NamePlaceholder, type: .latin(capitalization: .words), error: error, textUpdated: { text in params.updateText(.firstName, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .middleName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_MiddleName, text: value, placeholder: strings.Passport_Identity_MiddleNamePlaceholder, type: .latin(capitalization: .words), error: error, textUpdated: { text in params.updateText(.middleName, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .lastName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_Surname, text: value, placeholder: strings.Passport_Identity_SurnamePlaceholder, type: .latin(capitalization: .words), error: error, textUpdated: { text in params.updateText(.lastName, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .nativeInfoHeader(language): let title: String var value: String? if !language.isEmpty { let key = "Passport.Language.\(language)" if let string = strings.primaryComponent.dict[key] { value = string } else if let string = strings.secondaryComponent?.dict[key] { value = string } } if let value = value { title = strings.Passport_Identity_NativeNameTitle(value).0.uppercased() } else { title = strings.Passport_Identity_NativeNameGenericTitle } return FormControllerHeaderItem(text: title) case let .nativeFirstName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_Name, text: value, placeholder: strings.Passport_Identity_NamePlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.nativeFirstName, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .nativeMiddleName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_MiddleName, text: value, placeholder: strings.Passport_Identity_MiddleNamePlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.nativeMiddleName, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .nativeLastName(value, error): return FormControllerTextInputItem(title: strings.Passport_Identity_Surname, text: value, placeholder: strings.Passport_Identity_SurnamePlaceholder, type: .regular(capitalization: .words, autocorrection: false), returnKeyType: .`default`, error: error, textUpdated: { text in params.updateText(.nativeLastName, text) }, returnPressed: { params.endEditing() }) case let .nativeInfo(language, countryCode): let text: String var value: String? if !language.isEmpty { let key = "Passport.Language.\(language)" if let string = strings.primaryComponent.dict[key] { value = string } else if let string = strings.secondaryComponent?.dict[key] { value = string } } if let _ = value { text = strings.Passport_Identity_NativeNameHelp } else { let countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(countryCode.uppercased(), strings: strings) ?? "" text = strings.Passport_Identity_NativeNameGenericHelp(countryName).0 } return FormControllerTextItem(text: text) case let .gender(value, error): var text = "" if let value = value { switch value { case .male: text = strings.Passport_Identity_GenderMale case .female: text = strings.Passport_Identity_GenderFemale } } return FormControllerDetailActionItem(title: strings.Passport_Identity_Gender, text: text, placeholder: strings.Passport_Identity_GenderPlaceholder, error: error, activated: { params.activateSelection(.gender) }) case let .countryCode(category, value, error): let title: String let placeholder: String switch category { case .identity: title = strings.Passport_Identity_Country placeholder = strings.Passport_Identity_CountryPlaceholder case .address: title = strings.Passport_Address_Country placeholder = strings.Passport_Address_CountryPlaceholder } return FormControllerDetailActionItem(title: title, text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: placeholder, error: error, activated: { params.activateSelection(.country) }) case let .residenceCountryCode(value, error): return FormControllerDetailActionItem(title: strings.Passport_Identity_ResidenceCountry, text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: strings.Passport_Identity_ResidenceCountryPlaceholder, error: error, activated: { params.activateSelection(.residenceCountry) }) case let .birthdate(value, error): return FormControllerDetailActionItem(title: strings.Passport_Identity_DateOfBirth, text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: strings.Passport_Identity_DateOfBirthPlaceholder, error: error, activated: { params.activateSelection(.date(value?.timestamp, .birthdate)) }) case let .expiryDate(value, error): let title: String switch value { case .notSet: title = "" case .doesNotExpire: title = strings.Passport_Identity_ExpiryDateNone case let .date(date): title = stringForDate(timestamp: date.timestamp, strings: strings) } return FormControllerDetailActionItem(title: strings.Passport_Identity_ExpiryDate, text: title, placeholder: strings.Passport_Identity_ExpiryDatePlaceholder, error: error, activated: { let timestamp: Int32? switch value { case .notSet, .doesNotExpire: timestamp = nil case let .date(date): timestamp = date.timestamp } params.activateSelection(.date(timestamp, .expiry)) }) case let .deleteDocument(category, hasDocument): var title = strings.Passport_DeleteDocument if !hasDocument { switch category { case .identity: title = strings.Passport_DeletePersonalDetails case .address: title = strings.Passport_DeleteAddress } } return FormControllerActionItem(type: .destructive, title: title, activated: { params.deleteValue() }) case let .street1(value, error): return FormControllerTextInputItem(title: strings.Passport_Address_Street, text: value, placeholder: strings.Passport_Address_Street1Placeholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.street1, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .street2(value, error): return FormControllerTextInputItem(title: "", text: value, placeholder: strings.Passport_Address_Street2Placeholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.street2, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .city(value, error): return FormControllerTextInputItem(title: strings.Passport_Address_City, text: value, placeholder: strings.Passport_Address_CityPlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.city, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .state(value, error): return FormControllerTextInputItem(title: strings.Passport_Address_Region, text: value, placeholder: strings.Passport_Address_RegionPlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.state, text) }, returnPressed: { params.selectNextInputItem(self) }) case let .postcode(value, error): let color: FormControllerTextInputItemColor if value.count > 12 { color = .error } else { color = .primary } return FormControllerTextInputItem(title: strings.Passport_Address_Postcode, text: value, placeholder: strings.Passport_Address_PostcodePlaceholder, color: color, type: .latin(capitalization: .allCharacters), returnKeyType: .`default`, error: error, textUpdated: { text in params.updateText(.postcode, text) }, returnPressed: { params.endEditing() }) case .requestedDocumentsHeader: return FormControllerHeaderItem(text: strings.Passport_Identity_FilesTitle) case let .selfie(_, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { label = .text(strings.Passport_Identity_SelfieHelp) } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputSelfie"), title: strings.Passport_Identity_Selfie, label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.selfie) } }, deleted: { if let document = document { params.deleteDocument(document) } }) case let .frontSide(_, type, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { switch type { case .passport?, .internalPassport?: label = .text(strings.Passport_Identity_MainPageHelp) default: label = .text(strings.Passport_Identity_FrontSideHelp) } } let title: String let placeholder: UIImage? switch type { case .passport?, .internalPassport?: title = strings.Passport_Identity_MainPage placeholder = UIImage(bundleImageName: "Secure ID/PassportInputFrontSide") case .driversLicense?: title = strings.Passport_Identity_FrontSide placeholder = UIImage(bundleImageName: "Secure ID/DriversLicenseInputFrontSide") default: title = strings.Passport_Identity_FrontSide placeholder = UIImage(bundleImageName: "Secure ID/IdCardInputFrontSide") } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: placeholder, title: title, label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.frontSide(type)) } }, deleted: { if let document = document { params.deleteDocument(document) } }) case let .backSide(_, type, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { label = .text(strings.Passport_Identity_ReverseSideHelp) } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputBackSide"), title: strings.Passport_Identity_ReverseSide, label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.backSide(type)) } }, deleted: { if let document = document { params.deleteDocument(document) } }) case let .documentsInfo(category): let text: String switch category { case .identity: text = strings.Passport_Identity_ScansHelp case .address: text = strings.Passport_Address_ScansHelp } return FormControllerTextItem(text: text) case .translationsHeader: return FormControllerHeaderItem(text: strings.Passport_Identity_Translations) case let .translation(index, document, error): return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: nil, title: strings.Passport_Scans_ScanIndex("\(index + 1)").0, label: error.flatMap(SecureIdValueFormFileItemLabel.error) ?? .timestamp, activated: { params.openDocument(document) }, deleted: { params.deleteDocument(document) }) case let .addTranslation(hasAny): return FormControllerActionItem(type: .accent, title: hasAny ? strings.Passport_Scans_UploadNew : strings.Passport_Scans_Upload, fullTopInset: true, activated: { params.addFile(.translation) }) case .translationsInfo: return FormControllerTextItem(text: strings.Passport_Identity_TranslationsHelp) case let .error(_, text, _): return FormControllerTextItem(text: text, color: .error) } } } struct SecureIdDocumentFormControllerNodeInitParams { let context: AccountContext let secureIdContext: SecureIdAccessContext } final class SecureIdDocumentFormControllerNode: FormControllerNode { private var _itemParams: SecureIdDocumentFormParams? override var itemParams: SecureIdDocumentFormParams { return self._itemParams! } private var presentationData: PresentationData private var theme: PresentationTheme private var strings: PresentationStrings private let context: AccountContext private let secureIdContext: SecureIdAccessContext private let uploadContext: SecureIdVerificationDocumentsContext var actionInputStateUpdated: ((SecureIdDocumentFormInputState) -> Void)? var completedWithValues: (([SecureIdValueWithContext]?) -> Void)? var dismiss: (() -> Void)? var initiallyScrollTo: SecureIdDocumentFormScrollToSubject? private let actionDisposable = MetaDisposable() private let hiddenItemDisposable = MetaDisposable() required init(initParams: SecureIdDocumentFormControllerNodeInitParams, presentationData: PresentationData) { self.presentationData = presentationData self.theme = presentationData.theme self.strings = presentationData.strings self.context = initParams.context self.secureIdContext = initParams.secureIdContext var updateImpl: ((Int64, SecureIdVerificationLocalDocumentState) -> Void)? self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in updateImpl?(id, state) }) super.init(initParams: initParams, presentationData: presentationData) self._itemParams = SecureIdDocumentFormParams(account: self.context.account, context: self.secureIdContext, addFile: { [weak self] type in if let strongSelf = self { strongSelf.view.endEditing(true) strongSelf.presentAssetPicker(type) } }, openDocument: { [weak self] document in if let strongSelf = self { strongSelf.openDocument(document: document) } }, deleteDocument: { [weak self] document in if let strongSelf = self { strongSelf.deleteDocument(document: document) } }, updateText: { [weak self] field, value in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.documentState.updateTextField(type: field, value: value) var valueKey: SecureIdValueKey? var errorKey: SecureIdValueContentErrorKey? switch innerState.documentState { case let .identity(identity): switch field { case .firstName: valueKey = .personalDetails errorKey = .field(.personalDetails(.firstName)) case .lastName: valueKey = .personalDetails errorKey = .field(.personalDetails(.lastName)) case .middleName: valueKey = .personalDetails errorKey = .field(.personalDetails(.middleName)) case .nativeFirstName: valueKey = .personalDetails errorKey = .field(.personalDetails(.firstNameNative)) case .nativeLastName: valueKey = .personalDetails errorKey = .field(.personalDetails(.lastNameNative)) case .nativeMiddleName: valueKey = .personalDetails errorKey = .field(.personalDetails(.middleNameNative)) case .identifier: if let document = identity.document { switch document.type { case .passport: valueKey = .passport errorKey = .field(.passport(.documentId)) case .internalPassport: valueKey = .internalPassport errorKey = .field(.internalPassport(.documentId)) case .driversLicense: valueKey = .driversLicense errorKey = .field(.driversLicense(.documentId)) case .idCard: valueKey = .idCard errorKey = .field(.idCard(.documentId)) } } default: break } case .address: switch field { case .street1: valueKey = .address errorKey = .field(.address(.streetLine1)) case .street2: valueKey = .address errorKey = .field(.address(.streetLine2)) case .state: valueKey = .address errorKey = .field(.address(.state)) case .postcode: valueKey = .address errorKey = .field(.address(.postCode)) case .city: valueKey = .address errorKey = .field(.address(.city)) default: break } } if let valueKey = valueKey, let errorKey = errorKey { let valueErrorKey: SecureIdValueContentErrorKey = .value(valueKey) if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey, valueErrorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } }, selectNextInputItem: { [weak self] entry in guard let strongSelf = self else { return } var useNext = false strongSelf.enumerateItemsAndEntries({ itemEntry, itemNode in if itemEntry.isEqual(to: entry) { useNext = true } else if useNext { if case .deleteDocument = itemEntry { return false } else if let inputNode = itemNode as? FormControllerTextInputItemNode { inputNode.activate() return false } else if let actionNode = itemNode as? FormControllerDetailActionItemNode { actionNode.activate() return false } } return true }) strongSelf.forceUpdateState(transition: .animated(duration: 0.2, curve: .spring)) }, endEditing: { [weak self] in guard let strongSelf = self else { return } strongSelf.view.endEditing(true) }, activateSelection: { [weak self] field in if let strongSelf = self { switch field { case .country: let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme, displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.documentState.updateCountryCode(value: id) var valueKey: SecureIdValueKey? var errorKey: SecureIdValueContentErrorKey? switch innerState.documentState { case .identity: valueKey = .personalDetails errorKey = .field(.personalDetails(.countryCode)) case .address: valueKey = .address errorKey = .field(.address(.countryCode)) } if let valueKey = valueKey, let errorKey = errorKey { let valueErrorKey: SecureIdValueContentErrorKey = .value(valueKey) if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey, valueErrorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } strongSelf.view.endEditing(true) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case .residenceCountry: let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme, displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.documentState.updateResidenceCountryCode(value: id) var valueKey: SecureIdValueKey? var errorKey: SecureIdValueContentErrorKey? switch innerState.documentState { case .identity: valueKey = .personalDetails errorKey = .field(.personalDetails(.residenceCountryCode)) case .address: break } if let valueKey = valueKey, let errorKey = errorKey { let valueErrorKey: SecureIdValueContentErrorKey = .value(valueKey) if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey, valueErrorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } strongSelf.view.endEditing(true) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case let .date(current, field): var emptyTitle: String? var minimumDate: Date? = nil var maximumDate: Date? = nil let calendar = Calendar(identifier: .gregorian) let now = Date() var title: String? = nil if case .expiry = field { title = strongSelf.strings.Passport_Identity_ExpiryDate emptyTitle = strongSelf.strings.Passport_Identity_DoesNotExpire var deltaComponents = DateComponents() deltaComponents.month = 6 minimumDate = calendar.date(byAdding: deltaComponents, to: now) } else if case .birthdate = field { title = strongSelf.strings.Passport_Identity_DateOfBirth var components = calendar.dateComponents([.year, .month, .day], from: now) if let year = components.year { components.year = year - 18 components.hour = 0 components.minute = 0 maximumDate = calendar.date(from: components) } } let controller = DateSelectionActionSheetController(context: strongSelf.context, title: title, currentValue: current ?? Int32(Date().timeIntervalSince1970), minimumDate: minimumDate, maximumDate: maximumDate, emptyTitle: emptyTitle, applyValue: { value in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.documentState.updateDateField(type: field, value: value.flatMap(SecureIdDate.init)) var valueKey: SecureIdValueKey? var errorKey: SecureIdValueContentErrorKey? switch innerState.documentState { case let .identity(identity): switch field { case .birthdate: valueKey = .personalDetails errorKey = .field(.personalDetails(.birthdate)) case .expiry: if let document = identity.document { switch document.type { case .passport: valueKey = .passport errorKey = .field(.passport(.expiryDate)) case .internalPassport: valueKey = .internalPassport errorKey = .field(.internalPassport(.expiryDate)) case .driversLicense: valueKey = .driversLicense errorKey = .field(.driversLicense(.expiryDate)) case .idCard: valueKey = .idCard errorKey = .field(.idCard(.expiryDate)) } } } case .address: break } if let valueKey = valueKey, let errorKey = errorKey { let valueErrorKey: SecureIdValueContentErrorKey = .value(valueKey) if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey, valueErrorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } }) strongSelf.view.endEditing(true) strongSelf.present(controller, nil) case .gender: let controller = ActionSheetController(presentationData: strongSelf.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let applyAction: (SecureIdGender) -> Void = { gender in if let strongSelf = self, var innerState = strongSelf.innerState { innerState.documentState.updateGenderField(type: .gender, value: gender) var valueKey: SecureIdValueKey? var errorKey: SecureIdValueContentErrorKey? valueKey = .personalDetails errorKey = .field(.personalDetails(.gender)) if let valueKey = valueKey, let errorKey = errorKey { let valueErrorKey: SecureIdValueContentErrorKey = .value(valueKey) if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey, valueErrorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.strings.Passport_Identity_GenderMale, action: { dismissAction() applyAction(.male) }), ActionSheetButtonItem(title: strongSelf.strings.Passport_Identity_GenderFemale, action: { dismissAction() applyAction(.female) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.view.endEditing(true) strongSelf.present(controller, nil) } } }, scanPassport: { [weak self] in if let strongSelf = self { let controller = legacySecureIdScanController(theme: strongSelf.theme, strings: strongSelf.strings, finished: { recognizedData in if let strongSelf = self, let recognizedData = recognizedData, var innerState = strongSelf.innerState { innerState.documentState.updateWithRecognizedData(recognizedData) strongSelf.updateInnerState(transition: .immediate, with: innerState) } }) strongSelf.present(controller, nil) } }, deleteValue: { [weak self] in if let strongSelf = self { strongSelf.deleteValue() } }) updateImpl = { [weak self] id, state in if let strongSelf = self, var innerState = strongSelf.innerState { outer: for i in 0 ..< innerState.documents.count { switch innerState.documents[i] { case var .local(local): if local.id == id { local.state = state innerState.documents[i] = .local(local) break outer } case .remote: break } } if let selfieDocument = innerState.selfieDocument { switch selfieDocument { case var .local(local): if local.id == id { local.state = state innerState.selfieDocument = .local(local) } case .remote: break } } if let frontSideDocument = innerState.frontSideDocument { switch frontSideDocument { case var .local(local): if local.id == id { local.state = state innerState.frontSideDocument = .local(local) } case .remote: break } } if let backSideDocument = innerState.backSideDocument { switch backSideDocument { case var .local(local): if local.id == id { local.state = state innerState.backSideDocument = .local(local) } case .remote: break } } outer: for i in 0 ..< innerState.translations.count { switch innerState.translations[i] { case var .local(local): if local.id == id { local.state = state innerState.translations[i] = .local(local) break outer } case .remote: break } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } } deinit { self.actionDisposable.dispose() } private func presentAssetPicker(_ type: SecureIdAddFileTarget, replaceDocumentId: SecureIdVerificationDocumentId? = nil) { guard let validLayout = self.layoutState?.layout else { return } let attachmentType: SecureIdAttachmentMenuType var recognizeDocumentData = false switch type { case .scan: attachmentType = .multiple if let innerState = self.innerState { switch innerState.documentState { case .identity: recognizeDocumentData = true default: break } } case let .backSide(type): switch type { case .idCard?, .driversLicense?: attachmentType = .idCard default: attachmentType = .generic } recognizeDocumentData = true case let .frontSide(type): switch type { case .idCard?, .driversLicense?: attachmentType = .idCard default: attachmentType = .generic } recognizeDocumentData = true case .selfie: attachmentType = .selfie case .translation: attachmentType = .multiple } presentLegacySecureIdAttachmentMenu(context: self.context, present: { [weak self] c in self?.view.endEditing(true) self?.present(c, nil) }, validLayout: validLayout, type: attachmentType, recognizeDocumentData: recognizeDocumentData, completion: { [weak self] resources, recognizedData in self?.addDocuments(type: type, resources: resources, recognizedData: recognizedData, removeDocumentId: replaceDocumentId) }) } func addDocuments(type: SecureIdAddFileTarget, resources: [TelegramMediaResource], recognizedData: SecureIdRecognizedDocumentData?, removeDocumentId: SecureIdVerificationDocumentId?) { guard var innerState = self.innerState else { return } switch type { case .scan: var addIndex = innerState.documents.count if let removeDocumentId = removeDocumentId { for i in 0 ..< innerState.documents.count { if innerState.documents[i].id == removeDocumentId { innerState.documents.remove(at: i) addIndex = i break } } } for resource in resources { let id = arc4random64() innerState.documents.insert(.local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0))), at: addIndex) addIndex += 1 } if innerState.documents.count > 20 { innerState.documents = Array(innerState.documents[0 ..< 20]) } case .selfie: if let removeDocumentId = removeDocumentId { innerState = removeDocumentWithId(innerState, id: removeDocumentId) } loop: for resource in resources { let id = arc4random64() innerState.selfieDocument = .local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0))) break loop } case .frontSide: if let removeDocumentId = removeDocumentId { innerState = removeDocumentWithId(innerState, id: removeDocumentId) } loop: for resource in resources { let id = arc4random64() innerState.frontSideDocument = .local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0))) break loop } case .backSide: if let removeDocumentId = removeDocumentId { innerState = removeDocumentWithId(innerState, id: removeDocumentId) } loop: for resource in resources { let id = arc4random64() innerState.backSideDocument = .local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0))) break loop } case .translation: var addIndex = innerState.translations.count if let removeDocumentId = removeDocumentId { for i in 0 ..< innerState.translations.count { if innerState.translations[i].id == removeDocumentId { innerState.translations.remove(at: i) addIndex = i break } } } for resource in resources { let id = arc4random64() innerState.translations.insert(.local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0))), at: addIndex) addIndex += 1 } if innerState.translations.count > 20 { innerState.translations = Array(innerState.documents[0 ..< 20]) } } if let recognizedData = recognizedData { innerState.documentState.updateWithRecognizedData(recognizedData) } self.updateInnerState(transition: .immediate, with: innerState) } override func updateInnerState(transition: ContainedViewLayoutTransition, with innerState: SecureIdDocumentFormState) { let previousActionInputState = self.innerState?.actionInputState() super.updateInnerState(transition: transition, with: innerState) var documents = innerState.documents if let selfieDocument = innerState.selfieDocument { documents.append(selfieDocument) } if let frontSideDocument = innerState.frontSideDocument { documents.append(frontSideDocument) } if let backSideDocument = innerState.backSideDocument { documents.append(backSideDocument) } documents.append(contentsOf: innerState.translations) self.uploadContext.stateUpdated(documents) let actionInputState = innerState.actionInputState() if previousActionInputState != actionInputState { self.actionInputStateUpdated?(actionInputState) } } func hasUnsavedData() -> Bool { guard var innerState = self.innerState else { return false } guard let values = innerState.makeValues(), !values.isEmpty else { return false } for (key, value) in values { if innerState.previousValues[key]?.value != value { return true } } for (key, _) in innerState.previousValues { if values[key] == nil { return true } } return false } func save() { guard var innerState = self.innerState else { return } guard case .none = innerState.actionState else { return } guard case .saveAvailable = innerState.actionInputState() else { return } guard let values = innerState.makeValues(), !values.isEmpty else { return } if !innerState.previousValues.isEmpty, values == innerState.previousValues.mapValues({ $0.value }) { self.dismiss?() return } innerState.actionState = .saving self.updateInnerState(transition: .immediate, with: innerState) var saveValues: [Signal] = [] for (_, value) in values { saveValues.append(saveSecureIdValue(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, value: value, uploadedFiles: self.uploadContext.uploadedFiles)) } self.actionDisposable.set((combineLatest(saveValues) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.completedWithValues?(result) } }, error: { [weak self] error in if let strongSelf = self { guard var innerState = strongSelf.innerState else { return } guard case .saving = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) } })) } func deleteValue() { guard let innerState = self.innerState, !innerState.previousValues.isEmpty else { return } guard case .none = innerState.actionState else { return } let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let text: String let title: String switch innerState.documentState { case let .identity(state) where state.details != nil: text = self.strings.Passport_DeletePersonalDetailsConfirmation title = self.strings.Passport_DeletePersonalDetails case let .address(state) where state.details != nil: text = self.strings.Passport_DeleteAddressConfirmation title = self.strings.Passport_DeleteAddress default: text = self.strings.Passport_DeleteDocumentConfirmation title = self.strings.Passport_DeleteDocument } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self] in dismissAction() guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState, !innerState.previousValues.isEmpty else { return } innerState.actionState = .deleting strongSelf.updateInnerState(transition: .immediate, with: innerState) strongSelf.actionDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set(innerState.previousValues.keys)) |> deliverOnMainQueue).start(error: { error in guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } guard case .deleting = innerState.actionState else { return } innerState.actionState = .none strongSelf.updateInnerState(transition: .immediate, with: innerState) }, completed: { [weak self] in self?.completedWithValues?([]) })) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.present(controller, nil) } private func openDocument(document: SecureIdVerificationDocument) { let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strings.Passport_Identity_FilesView, action: { [weak self] in dismissAction() self?.presentGallery(document: document) }), ActionSheetButtonItem(title: strings.Passport_Identity_FilesUploadNew, action: { [weak self] in dismissAction() guard let strongSelf = self else { return } guard let innerState = strongSelf.innerState else { return } var target: SecureIdAddFileTarget? let id = document.id if let selfieDocument = innerState.selfieDocument, selfieDocument.id == id { target = .selfie } if let frontSideDocument = innerState.frontSideDocument, frontSideDocument.id == id { switch innerState.documentState { case let .identity(identity): if let document = identity.document { target = .frontSide(document.type) } case .address: break } } if let backSideDocument = innerState.backSideDocument, backSideDocument.id == id { switch innerState.documentState { case let .identity(identity): if let document = identity.document { target = .backSide(document.type) } case .address: break } } for i in 0 ..< innerState.documents.count { if innerState.documents[i].id == id { target = .scan break } } for i in 0 ..< innerState.translations.count { if innerState.translations[i].id == id { target = .translation break } } if let target = target { strongSelf.view.endEditing(true) strongSelf.presentAssetPicker(target, replaceDocumentId: document.id) } }), ActionSheetButtonItem(title: strings.Common_Delete, color: .destructive, action: { [weak self] in dismissAction() guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } innerState = removeDocumentWithId(innerState, id: document.id) strongSelf.updateInnerState(transition: .immediate, with: innerState) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.present(controller, nil) } private func deleteDocument(document: SecureIdVerificationDocument) { guard var innerState = self.innerState else { return } innerState = removeDocumentWithId(innerState, id: document.id) self.updateInnerState(transition: .animated(duration: 0.2, curve: .spring), with: innerState) } private func presentGallery(document: SecureIdVerificationDocument) { guard let innerState = self.innerState else { return } var entries: [SecureIdDocumentGalleryEntry] = [] var index = 0 var centralIndex = 0 var totalCount: Int32 = 0 if innerState.frontSideDocument != nil { totalCount += 1 } if innerState.backSideDocument != nil { totalCount += 1 } if innerState.selfieDocument != nil { totalCount += 1 } totalCount += Int32(innerState.documents.count) totalCount += Int32(innerState.translations.count) if let frontSideDocument = innerState.frontSideDocument { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: frontSideDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: totalCount), error: "")) if document.id == frontSideDocument.id { centralIndex = index } index += 1 } if let backSideDocument = innerState.backSideDocument { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: backSideDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: totalCount), error: "")) if document.id == backSideDocument.id { centralIndex = index } index += 1 } if let selfieDocument = innerState.selfieDocument { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: selfieDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: totalCount), error: "")) if document.id == selfieDocument.id { centralIndex = index } index += 1 } if let _ = innerState.documents.firstIndex(where: { $0.id == document.id }) { for itemDocument in innerState.documents { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: itemDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: totalCount), error: "")) if document.id == itemDocument.id { centralIndex = index } index += 1 } } if let _ = innerState.translations.firstIndex(where: { $0.id == document.id }) { for itemDocument in innerState.translations { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: itemDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: totalCount), error: "")) if document.id == itemDocument.id { centralIndex = index } index += 1 } } let galleryController = SecureIdDocumentGalleryController(context: self.context, secureIdContext: self.secureIdContext, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in }) galleryController.deleteResource = { [weak self] resource in guard let strongSelf = self else { return } guard var innerState = strongSelf.innerState else { return } if let selfieDocument = innerState.selfieDocument, selfieDocument.resource.isEqual(to: resource) { innerState.selfieDocument = nil } if let frontSideDocument = innerState.frontSideDocument, frontSideDocument.resource.isEqual(to: resource) { innerState.frontSideDocument = nil } if let backSideDocument = innerState.backSideDocument, backSideDocument.resource.isEqual(to: resource) { innerState.backSideDocument = nil } for i in 0 ..< innerState.documents.count { if innerState.documents[i].resource.isEqual(to: resource) { innerState.documents.remove(at: i) break } } for i in 0 ..< innerState.translations.count { if innerState.translations[i].resource.isEqual(to: resource) { innerState.translations.remove(at: i) break } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } self.hiddenItemDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in guard let strongSelf = self else { return } for itemNode in strongSelf.itemNodes { if let itemNode = itemNode as? SecureIdValueFormFileItemNode, let item = itemNode.item { if let entry = entry, let document = item.document, document.resource.isEqual(to: entry.resource) { itemNode.imageNode.isHidden = true } else { itemNode.imageNode.isHidden = false } } } })) self.view.endEditing(true) self.present(galleryController, SecureIdDocumentGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry in guard let strongSelf = self else { return nil } for itemNode in strongSelf.itemNodes { if let itemNode = itemNode as? SecureIdValueFormFileItemNode, let item = itemNode.item, let document = item.document { if document.resource.isEqual(to: entry.resource) { return GalleryTransitionArguments(transitionNode: (itemNode.imageNode, { return (itemNode.imageNode.view.snapshotContentTree(unhide: true), nil) }), addToTransitionSurface: { view in self?.view.addSubview(view) }) } } } return nil })) } override func didAppear() { if let scrollTo = self.initiallyScrollTo { self.scrollTo(scrollTo) } } func scrollTo(_ subject: SecureIdDocumentFormScrollToSubject) { self.enumerateItemsAndEntries { entry, itemNode -> Bool in switch subject { case .selfie: if case .selfie = entry { self.scrollToItemNode(itemNode) return false } case .translation: if case .translationsHeader = entry { self.scrollToItemNode(itemNode) return false } } return true } } }