import Foundation import AsyncDisplayKit import Display import TelegramCore import Postbox import SwiftSignalKit private enum SecureIdDocumentFormTextField { case identifier case firstName case lastName 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 } private enum AddFileTarget { case scan case selfie case frontSide case backSide } final class SecureIdDocumentFormParams { fileprivate let account: Account fileprivate let context: SecureIdAccessContext fileprivate let addFile: (AddFileTarget) -> Void fileprivate let openDocument: (SecureIdVerificationDocument) -> Void fileprivate let updateText: (SecureIdDocumentFormTextField, String) -> Void fileprivate let activateSelection: (SecureIdDocumentFormSelectionField) -> Void fileprivate let deleteValue: () -> Void fileprivate init(account: Account, context: SecureIdAccessContext, addFile: @escaping (AddFileTarget) -> Void, openDocument: @escaping (SecureIdVerificationDocument) -> Void, updateText: @escaping (SecureIdDocumentFormTextField, String) -> Void, activateSelection: @escaping (SecureIdDocumentFormSelectionField) -> Void, deleteValue: @escaping () -> Void) { self.account = account self.context = context self.addFile = addFile self.openDocument = openDocument self.updateText = updateText self.activateSelection = activateSelection self.deleteValue = deleteValue } } private struct SecureIdDocumentFormIdentityDetailsState: Equatable { var firstName: String var lastName: String var countryCode: String var residenceCountryCode: String var birthdate: SecureIdDate? var gender: SecureIdGender? func isComplete() -> Bool { if self.firstName.isEmpty { return false } if self.lastName.isEmpty { 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 } } private struct SecureIdDocumentFormIdentityDocumentState: Equatable { var type: SecureIdRequestedIdentityDocument var identifier: String var expiryDate: SecureIdDate? func isComplete() -> Bool { if self.identifier.isEmpty { 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 { if self.street1.isEmpty { return false } if self.city.isEmpty { return false } if self.countryCode.isEmpty { return false } if self.postcode.isEmpty { 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 .lastName: state.details?.lastName = 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: state.document?.expiryDate = value } 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 } } } } private enum SecureIdDocumentFormActionState { case none case saving case deleting } enum SecureIdDocumentFormInputState { case saveAvailable case saveNotAvailable case inProgress } 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 actionState: SecureIdDocumentFormActionState 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 } return true } func entries() -> [FormControllerItemEntry] { switch self.documentState { case let .identity(identity): var result: [FormControllerItemEntry] = [] var errorIndex = 0 if let document = identity.document, false { result.append(.entry(SecureIdDocumentFormEntry.scansHeader)) 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.documents { switch document { case .local: fileHashes = nil break loop case let .remote(file): fileHashes?.insert(file.fileHash) } } if let fileHashes = fileHashes, !fileHashes.isEmpty, let error = value.errors[.files(hashes: fileHashes)] { //result.append(.spacer) result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error))) errorIndex += 1 } } 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(.identity))) result.append(.spacer) } if let details = identity.details { result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstName))]))) 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(details.countryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.residenceCountryCode(details.residenceCountryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.residenceCountryCode))]))) } if let document = identity.document { if (identity.details == nil) { result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) } var identifierError: String? var expiryDateError: String? switch document.type { case .passport: identifierError = self.previousValues[.passport]?.errors[.field(.passport(.documentId))] expiryDateError = self.previousValues[.passport]?.errors[.field(.passport(.expiryDate))] case .internalPassport: identifierError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.documentId))] expiryDateError = self.previousValues[.internalPassport]?.errors[.field(.internalPassport(.expiryDate))] case .driversLicense: identifierError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.documentId))] expiryDateError = self.previousValues[.driversLicense]?.errors[.field(.driversLicense(.expiryDate))] case .idCard: identifierError = self.previousValues[.idCard]?.errors[.field(.idCard(.documentId))] expiryDateError = self.previousValues[.idCard]?.errors[.field(.idCard(.expiryDate))] } result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier, identifierError))) result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate, expiryDateError))) } if self.selfieRequired || self.frontSideRequired || self.backSideRequired { 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, document, error))) } else { result.append(.entry(SecureIdDocumentFormEntry.frontSide(1, 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, document, error))) } else { result.append(.entry(SecureIdDocumentFormEntry.backSide(2, nil, nil))) } } if self.selfieRequired { 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))) } } } if !self.previousValues.isEmpty { result.append(.spacer) result.append(.entry(SecureIdDocumentFormEntry.deleteDocument)) } return result case let .address(address): var result: [FormControllerItemEntry] = [] var errorIndex = 0 if let document = address.document { 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, let error = value.errors[.files(hashes: fileHashes)] { result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error))) errorIndex += 1 } } 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 details = address.details { result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.address))) 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(details.countryCode, self.previousValues[.address]?.errors[.field(.address(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode, self.previousValues[.address]?.errors[.field(.address(.postCode))]))) } if !self.previousValues.isEmpty { result.append(.spacer) result.append(.entry(SecureIdDocumentFormEntry.deleteDocument)) } return result } } func actionInputState() -> SecureIdDocumentFormInputState { switch self.actionState { case .deleting, .saving: return .inProgress default: break } switch self.documentState { case let .identity(identity): if !identity.isComplete() { return .saveNotAvailable } case let .address(address): if !address.isComplete() { return .saveNotAvailable } } if self.selfieRequired { if let selfieDocument = self.selfieDocument { switch selfieDocument { case let .local(local): switch local.state { case .uploading: return .saveNotAvailable case .uploaded: break } case .remote: break } } else { return .saveNotAvailable } } for document in self.documents { switch document { case let .local(local): switch local.state { case .uploading: return .saveNotAvailable case .uploaded: break } case .remote: break } } return .saveAvailable } } extension SecureIdDocumentFormState { init(requestedData: SecureIdDocumentFormRequestedData, values: [SecureIdValueKey: SecureIdValueWithContext]) { switch requestedData { case let .identity(details, document, selfie): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] var detailsState: SecureIdDocumentFormIdentityDetailsState? if details { if let value = values[.personalDetails], case let .personalDetails(personalDetailsValue) = value.value { previousValues[.personalDetails] = value detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: personalDetailsValue.firstName, lastName: personalDetailsValue.lastName, countryCode: personalDetailsValue.countryCode, residenceCountryCode: personalDetailsValue.residenceCountryCode, birthdate: personalDetailsValue.birthdate, gender: personalDetailsValue.gender) } else { detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: "", lastName: "", 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? if let document = document { var identifier: String = "" var expiryDate: SecureIdDate? 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 verificationDocuments = passport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = passport.selfieDocument.flatMap(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 verificationDocuments = internalPassport.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = internalPassport.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = internalPassport.frontSideDocument.flatMap(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 verificationDocuments = driversLicense.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = driversLicense.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = driversLicense.frontSideDocument.flatMap(SecureIdVerificationDocument.init) backSideDocument = driversLicense.backSideDocument.flatMap(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 verificationDocuments = idCard.verificationDocuments.compactMap(SecureIdVerificationDocument.init) selfieDocument = idCard.selfieDocument.flatMap(SecureIdVerificationDocument.init) frontSideDocument = idCard.frontSideDocument.flatMap(SecureIdVerificationDocument.init) backSideDocument = idCard.backSideDocument.flatMap(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, actionState: .none) case let .address(details, document): var previousValues: [SecureIdValueKey: SecureIdValueWithContext] = [:] var detailsState: SecureIdDocumentFormAddressDetailsState? var documentState: SecureIdRequestedAddressDocument? var verificationDocuments: [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) } case .temporaryRegistration: if let value = values[.temporaryRegistration], case let .temporaryRegistration(temporaryRegistration) = value.value { previousValues[value.value.key] = value verificationDocuments = temporaryRegistration.verificationDocuments.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) } case .utilityBill: if let value = values[.utilityBill], case let .utilityBill(utilityBill) = value.value { previousValues[value.value.key] = value verificationDocuments = utilityBill.verificationDocuments.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) } } 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, actionState: .none) } } 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 } } } 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(firstName: details.firstName, lastName: details.lastName, birthdate: birthdate, countryCode: details.countryCode, residenceCountryCode: details.residenceCountryCode, gender: gender)) } if let document = identity.document { guard !document.identifier.isEmpty else { return nil } switch document.type { case .passport: values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) case .internalPassport: values[.internalPassport] = .internalPassport(SecureIdInternalPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument)) case .driversLicense: values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument, frontSideDocument: frontSideDocument, backSideDocument: backSideDocument)) case .idCard: values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, 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 { switch document { case .passportRegistration: values[.passportRegistration] = .passportRegistration(SecureIdPassportRegistrationValue(verificationDocuments: verificationDocuments)) case .temporaryRegistration: values[.temporaryRegistration] = .temporaryRegistration(SecureIdTemporaryRegistrationValue(verificationDocuments: verificationDocuments)) case .bankStatement: values[.bankStatement] = .bankStatement(SecureIdBankStatementValue(verificationDocuments: verificationDocuments)) case .utilityBill: values[.utilityBill] = .utilityBill(SecureIdUtilityBillValue(verificationDocuments: verificationDocuments)) case .rentalAgreement: values[.rentalAgreement] = .rentalAgreement(SecureIdRentalAgreementValue(verificationDocuments: verificationDocuments)) } } return values } } } enum SecureIdDocumentFormEntryId: Hashable { case scansHeader case scan(SecureIdVerificationDocumentId) case addScan case scansInfo case infoHeader case identifier case firstName case lastName case gender case countryCode case residenceCountryCode case birthdate case expiryDate case deleteDocument case requestedDocumentsHeader case selfie case frontSide case backSide case documentsInfo case street1 case street2 case city case state case postcode case error } enum SecureIdDocumentFormEntryCategory { case identity case address } enum SecureIdDocumentFormEntry: FormControllerEntry { 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 lastName(String, String?) case gender(SecureIdGender?, String?) case countryCode(String, String?) case residenceCountryCode(String, String?) case birthdate(SecureIdDate?, String?) case expiryDate(SecureIdDate?, String?) case deleteDocument case requestedDocumentsHeader case selfie(Int, SecureIdVerificationDocument?, String?) case frontSide(Int, SecureIdVerificationDocument?, String?) case backSide(Int, SecureIdVerificationDocument?, String?) case documentsInfo case error(Int, String) 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 .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 .lastName: return .lastName 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 .error: return .error } } func isEqual(to: SecureIdDocumentFormEntry) -> Bool { switch self { 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 .lastName(value, error): if case .lastName(value, error) = 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(value, error): if case .countryCode(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 .deleteDocument: if case .deleteDocument = to { 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, document, error): if case .frontSide(index, document, error) = to { return true } else { return false } case let .backSide(index, document, error): if case .backSide(index, document, error) = to { return true } else { return false } case .documentsInfo: if case .documentsInfo = to { return true } else { return false } case let .error(index, text): if case .error(index, text) = to { return true } else { return false } } } func item(params: SecureIdDocumentFormParams, strings: PresentationStrings) -> FormControllerItem { switch self { case .scansHeader: return FormControllerHeaderItem(text: "SCANS") case let .scan(index, document, error): return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: nil, title: "Scan \(index + 1)", label: error.flatMap(SecureIdValueFormFileItemLabel.error) ?? .timestamp, activated: { params.openDocument(document) }) case let .addScan(hasAny): return FormControllerActionItem(type: .accent, title: hasAny ? "Upload Additional Scan" : "Upload Scan", fullTopInset: true, activated: { params.addFile(.scan) }) case let .scansInfo(type): let text: String switch type { case .identity: text = "The document must contain your photograph, name, surname, date of birth, citizenship, document issue date and document number." case .address: text = "The document must contain your first and last name, your residential address, a stamp / barcode / QR code / logo, and issue date, no more that 3 months ago." } return FormControllerTextItem(text: text) case let .infoHeader(type): let text: String switch type { case .identity: text = "DOCUMENT DETAILS" case .address: text = "ADDRESS" } return FormControllerHeaderItem(text: text) case let .identifier(value, error): return FormControllerTextInputItem(title: "Document #", text: value, placeholder: "Document Number", error: error, textUpdated: { text in params.updateText(.identifier, text) }) case let .firstName(value, error): return FormControllerTextInputItem(title: "First Name", text: value, placeholder: "First Name", error: error, textUpdated: { text in params.updateText(.firstName, text) }) case let .lastName(value, error): return FormControllerTextInputItem(title: "Last Name", text: value, placeholder: "Last Name", error: error, textUpdated: { text in params.updateText(.lastName, text) }) case let .gender(value, error): var text = "" if let value = value { switch value { case .male: text = "Male" case .female: text = "Female" } } return FormControllerDetailActionItem(title: "Gender", text: text, placeholder: "Gender", error: error, activated: { params.activateSelection(.gender) }) case let .countryCode(value, error): return FormControllerDetailActionItem(title: "Country", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Country", error: error, activated: { params.activateSelection(.country) }) case let .residenceCountryCode(value, error): return FormControllerDetailActionItem(title: "Residence", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Residence Country", error: error, activated: { params.activateSelection(.residenceCountry) }) case let .birthdate(value, error): return FormControllerDetailActionItem(title: "Date of Birth", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Date of Birth", error: error, activated: { params.activateSelection(.date(value?.timestamp, .birthdate)) }) case let .expiryDate(value, error): return FormControllerDetailActionItem(title: "Expiry Date", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Expiry Date", error: error, activated: { params.activateSelection(.date(value?.timestamp, .expiry)) }) case .deleteDocument: return FormControllerActionItem(type: .destructive, title: "Delete Document", activated: { params.deleteValue() }) case let .street1(value, error): return FormControllerTextInputItem(title: "Street", text: value, placeholder: "Street and number, P.O. box", error: error, textUpdated: { text in params.updateText(.street1, text) }) case let .street2(value, error): return FormControllerTextInputItem(title: "", text: value, placeholder: "Apt., suite, unit, builting, block", error: error, textUpdated: { text in params.updateText(.street2, text) }) case let .city(value, error): return FormControllerTextInputItem(title: "City", text: value, placeholder: "City", error: error, textUpdated: { text in params.updateText(.city, text) }) case let .state(value, error): return FormControllerTextInputItem(title: "Region", text: value, placeholder: "State / Province / Region", error: error, textUpdated: { text in params.updateText(.state, text) }) case let .postcode(value, error): return FormControllerTextInputItem(title: "Postcode", text: value, placeholder: "Postcode", error: error, textUpdated: { text in params.updateText(.postcode, text) }) case .requestedDocumentsHeader: return FormControllerHeaderItem(text: "REQUESTED FILES") case let .selfie(_, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { label = .text("Upload a selfie of yourself holding document") } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputSelfie"), title: "Selfie", label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.selfie) } }) case let .frontSide(_, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { label = .text("Upload a front side photo of a document") } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/PassportInputFrontSide"), title: "Front Side", label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.frontSide) } }) case let .backSide(_, document, error): let label: SecureIdValueFormFileItemLabel if let error = error { label = .error(error) } else if document != nil { label = .timestamp } else { label = .text("Upload a reverse side photo of a document") } return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, placeholder: UIImage(bundleImageName: "Secure ID/DocumentInputBackSide"), title: "Reverse Side", label: label, activated: { if let document = document { params.openDocument(document) } else { params.addFile(.backSide) } }) case .documentsInfo: return FormControllerTextItem(text: "") case let .error(_, text): return FormControllerTextItem(text: text, color: .error) } } } struct SecureIdDocumentFormControllerNodeInitParams { let account: Account let context: SecureIdAccessContext } final class SecureIdDocumentFormControllerNode: FormControllerNode { private var _itemParams: SecureIdDocumentFormParams? override var itemParams: SecureIdDocumentFormParams { return self._itemParams! } private var theme: PresentationTheme private var strings: PresentationStrings private let account: Account private let context: SecureIdAccessContext private let uploadContext: SecureIdVerificationDocumentsContext var actionInputStateUpdated: ((SecureIdDocumentFormInputState) -> Void)? var completedWithValues: (([SecureIdValueWithContext]?) -> Void)? var dismiss: (() -> Void)? private let actionDisposable = MetaDisposable() private let hiddenItemDisposable = MetaDisposable() required init(initParams: SecureIdDocumentFormControllerNodeInitParams, theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings self.account = initParams.account self.context = initParams.context var updateImpl: ((Int64, SecureIdVerificationLocalDocumentState) -> Void)? self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.account.postbox, network: self.account.network, context: self.context, update: { id, state in updateImpl?(id, state) }) super.init(initParams: initParams, theme: theme, strings: strings) self._itemParams = SecureIdDocumentFormParams(account: self.account, context: self.context, addFile: { [weak self] type in if let strongSelf = self { strongSelf.presentAssetPicker(type) } }, openDocument: { [weak self] document in if let strongSelf = self { strongSelf.presentGallery(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 .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)) default: break } } if let valueKey = valueKey, let errorKey = errorKey { if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } }, activateSelection: { [weak self] field in if let strongSelf = self { switch field { case .country: let controller = AuthorizationSequenceCountrySelectionController(strings: strings, theme: defaultLightAuthorizationTheme, 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 { if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } strongSelf.view.endEditing(true) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case .residenceCountry: let controller = AuthorizationSequenceCountrySelectionController(strings: strings, theme: defaultLightAuthorizationTheme, 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 { if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } strongSelf.view.endEditing(true) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case let .date(current, field): var emptyTitle: String? if case .expiry = field { emptyTitle = "Does not expire" } let controller = DateSelectionActionSheetController(theme: theme, strings: strings, currentValue: current ?? Int32(Date().timeIntervalSince1970), 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 { if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } }) strongSelf.view.endEditing(true) strongSelf.present(controller, nil) case .gender: let controller = ActionSheetController(presentationTheme: theme) 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 { if let previousValue = innerState.previousValues[valueKey] { innerState.previousValues[valueKey] = previousValue.withRemovedErrors([errorKey]) } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: "Male", action: { dismissAction() applyAction(.male) }), ActionSheetButtonItem(title: "Female", action: { dismissAction() applyAction(.female) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.view.endEditing(true) 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 } } strongSelf.updateInnerState(transition: .immediate, with: innerState) } } } deinit { self.actionDisposable.dispose() } private func presentAssetPicker(_ type: AddFileTarget) { guard let validLayout = self.layoutState?.layout else { return } let attachmentType: SecureIdAttachmentMenuType switch type { case .scan: attachmentType = .multiple case .backSide, .frontSide: attachmentType = .generic case .selfie: attachmentType = .selfie } presentLegacySecureIdAttachmentMenu(account: self.account, present: { [weak self] c in self?.present(c, nil) }, validLayout: validLayout, type: attachmentType, completion: { [weak self] resources, recognizedData in self?.addDocuments(type: type, resources: resources, recognizedData: recognizedData) }) } private func addDocuments(type: AddFileTarget, resources: [TelegramMediaResource], recognizedData: SecureIdRecognizedDocumentData?) { guard var innerState = self.innerState else { return } switch type { case .scan: for resource in resources { let id = arc4random64() innerState.documents.append(.local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), timestamp: Int32(Date().timeIntervalSince1970), state: .uploading(0.0)))) } case .selfie: 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: 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: 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 } } if let recognizedData = recognizedData { switch innerState.documentState { case var .identity(identity): if var document = identity.document { switch document.type { case .passport: break case .internalPassport: break case .driversLicense: break case .idCard: break } if var details = identity.details { if details.firstName.isEmpty { details.firstName = recognizedData.firstName ?? "" } if details.lastName.isEmpty { details.lastName = recognizedData.lastName ?? "" } if details.birthdate == nil, let birthdate = recognizedData.birthDate { details.birthdate = SecureIdDate(timestamp: Int32(birthdate.timeIntervalSince1970)) } if details.gender == nil, let gender = recognizedData.gender { if gender == "M" { details.gender = .male } else { details.gender = .female } } if details.countryCode.isEmpty { details.countryCode = recognizedData.issuingCountry ?? "" } identity.details = details } if document.identifier.isEmpty { document.identifier = recognizedData.documentNumber ?? "" } if document.expiryDate == nil, let expiryDate = recognizedData.expiryDate { document.expiryDate = SecureIdDate(timestamp: Int32(expiryDate.timeIntervalSince1970)) } identity.document = document innerState.documentState = .identity(identity) } default: break } } 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) } self.uploadContext.stateUpdated(documents) let actionInputState = innerState.actionInputState() if previousActionInputState != actionInputState { self.actionInputStateUpdated?(actionInputState) } } 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.account.postbox, network: self.account.network, context: self.context, 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 var innerState = self.innerState, !innerState.previousValues.isEmpty else { return } guard case .none = innerState.actionState else { return } innerState.actionState = .deleting self.updateInnerState(transition: .immediate, with: innerState) self.actionDisposable.set((deleteSecureIdValues(network: self.account.network, keys: Set(innerState.previousValues.keys)) |> deliverOnMainQueue).start(error: { [weak self] error in if let strongSelf = self { 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 if let strongSelf = self { strongSelf.completedWithValues?([]) } })) } private func presentGallery(document: SecureIdVerificationDocument) { guard let innerState = self.innerState else { return } var entries: [SecureIdDocumentGalleryEntry] = [] var index = 0 var centralIndex = 0 if let selfieDocument = innerState.selfieDocument, selfieDocument.id == document.id { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: selfieDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: 1), error: "")) centralIndex = index index += 1 } else { for itemDocument in innerState.documents { entries.append(SecureIdDocumentGalleryEntry(index: Int32(index), resource: itemDocument.resource, location: SecureIdDocumentGalleryEntryLocation(position: Int32(index), totalCount: Int32(innerState.documents.count)), error: "")) if document.id == itemDocument.id { centralIndex = index } index += 1 } } let galleryController = SecureIdDocumentGalleryController(account: self.account, context: self.context, 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.selfieDocument = nil } for i in 0 ..< innerState.documents.count { if innerState.documents[i].resource.isEqual(to: resource) { innerState.documents.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.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) }), addToTransitionSurface: { view in self?.view.addSubview(view) }) } } } return nil })) } }