Swiftgram/TelegramUI/SecureIdDocumentFormControllerNode.swift
2018-04-11 23:43:31 +04:00

1365 lines
57 KiB
Swift

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 date(Int32?, SecureIdDocumentFormDateField)
case gender
}
private enum AddFileTarget {
case scan
case selfie
}
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 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.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 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 let previousValues: [SecureIdValueKey: SecureIdValueWithContext]
fileprivate var documentState: SecureIdDocumentFormDocumentState
fileprivate var documents: [SecureIdVerificationDocument]
fileprivate var selfieRequired: Bool
fileprivate var selfieDocument: SecureIdVerificationDocument?
fileprivate var actionState: SecureIdDocumentFormActionState
fileprivate var errors: [SecureIdErrorKey: [String]]
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
}
return true
}
func entries() -> [FormControllerItemEntry<SecureIdDocumentFormEntry>] {
switch self.documentState {
case let .identity(identity):
var result: [FormControllerItemEntry<SecureIdDocumentFormEntry>] = []
var errorIndex = 0
if let errors = self.errors[.personalDetails], !errors.isEmpty {
result.append(.spacer)
for error in errors {
result.append(.entry(SecureIdDocumentFormEntry.error(errorIndex, error)))
errorIndex += 1
}
result.append(.spacer)
}
if let _ = identity.document {
result.append(.entry(SecureIdDocumentFormEntry.scansHeader))
for i in 0 ..< self.documents.count {
result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i])))
}
result.append(.entry(SecureIdDocumentFormEntry.addScan(!self.documents.isEmpty)))
result.append(.entry(SecureIdDocumentFormEntry.scansInfo(.identity)))
result.append(.spacer)
}
if self.selfieRequired {
result.append(.entry(SecureIdDocumentFormEntry.selfieHeader))
if let document = self.selfieDocument {
result.append(.entry(SecureIdDocumentFormEntry.selfie(0, document)))
}
result.append(.entry(SecureIdDocumentFormEntry.addSelfie))
result.append(.entry(SecureIdDocumentFormEntry.selfieInfo))
result.append(.spacer)
}
if let details = identity.details {
result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity)))
result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName)))
result.append(.entry(SecureIdDocumentFormEntry.lastName(details.lastName)))
result.append(.entry(SecureIdDocumentFormEntry.gender(details.gender)))
result.append(.entry(SecureIdDocumentFormEntry.birthdate(details.birthdate)))
result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode)))
}
if let document = identity.document {
result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier)))
result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate)))
if !self.previousValues.isEmpty {
result.append(.spacer)
result.append(.entry(SecureIdDocumentFormEntry.deleteDocument))
}
}
return result
case let .address(address):
var result: [FormControllerItemEntry<SecureIdDocumentFormEntry>] = []
if let _ = address.document {
result.append(.entry(SecureIdDocumentFormEntry.scansHeader))
for i in 0 ..< self.documents.count {
result.append(.entry(SecureIdDocumentFormEntry.scan(i, self.documents[i])))
}
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)))
result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2)))
result.append(.entry(SecureIdDocumentFormEntry.city(details.city)))
result.append(.entry(SecureIdDocumentFormEntry.state(details.state)))
result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode)))
result.append(.entry(SecureIdDocumentFormEntry.postcode(details.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], errors: [SecureIdErrorKey: [String]]) {
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, birthdate: personalDetailsValue.birthdate, gender: personalDetailsValue.gender)
} else {
detailsState = SecureIdDocumentFormIdentityDetailsState(firstName: "", lastName: "", countryCode: "", birthdate: nil, gender: nil)
}
}
var documentState: SecureIdDocumentFormIdentityDocumentState?
var verificationDocuments: [SecureIdVerificationDocument] = []
var selfieDocument: 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)
}
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)
}
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)
}
}
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, actionState: .none, errors: errors)
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 .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, actionState: .none, errors: errors)
}
}
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
}
}
}
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 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, 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))
case .driversLicense:
values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
case .idCard:
values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
}
}
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 .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 birthdate
case expiryDate
case deleteDocument
case selfieHeader
case selfie
case addSelfie
case selfieInfo
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)
case addScan(Bool)
case scansInfo(SecureIdDocumentFormEntryCategory)
case infoHeader(SecureIdDocumentFormEntryCategory)
case identifier(String)
case firstName(String)
case lastName(String)
case gender(SecureIdGender?)
case countryCode(String)
case birthdate(SecureIdDate?)
case expiryDate(SecureIdDate?)
case deleteDocument
case selfieHeader
case selfie(Int, SecureIdVerificationDocument)
case addSelfie
case selfieInfo
case error(Int, String)
case street1(String)
case street2(String)
case city(String)
case state(String)
case postcode(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 .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 .selfieHeader:
return .selfieHeader
case .selfie:
return .selfie
case .addSelfie:
return .addSelfie
case .selfieInfo:
return .selfieInfo
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):
if case let .scan(rhsId, rhsDocument) = to, lhsId == rhsId, lhsDocument == rhsDocument {
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):
if case .identifier(value) = to {
return true
} else {
return false
}
case let .firstName(value):
if case .firstName(value) = to {
return true
} else {
return false
}
case let .lastName(value):
if case .lastName(value) = to {
return true
} else {
return false
}
case let .gender(value):
if case .gender(value) = to {
return true
} else {
return false
}
case let .countryCode(value):
if case .countryCode(value) = to {
return true
} else {
return false
}
case let .birthdate(lhsValue):
if case let .birthdate(rhsValue) = to, lhsValue == rhsValue {
return true
} else {
return false
}
case let .expiryDate(lhsValue):
if case let .expiryDate(rhsValue) = to, lhsValue == rhsValue {
return true
} else {
return false
}
case .deleteDocument:
if case .deleteDocument = to {
return true
} else {
return false
}
case let .street1(value):
if case .street1(value) = to {
return true
} else {
return false
}
case let .street2(value):
if case .street2(value) = to {
return true
} else {
return false
}
case let .city(value):
if case .city(value) = to {
return true
} else {
return false
}
case let .state(value):
if case .state(value) = to {
return true
} else {
return false
}
case let .postcode(value):
if case .postcode(value) = to {
return true
} else {
return false
}
case .selfieHeader:
if case .selfieHeader = to {
return true
} else {
return false
}
case let .selfie(index, document):
if case .selfie(index, document) = to {
return true
} else {
return false
}
case .addSelfie:
if case .addSelfie = to {
return true
} else {
return false
}
case .selfieInfo:
if case .selfieInfo = to {
return true
} else {
return false
}
case .error:
if case .error = 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):
return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, title: "Scan \(index + 1)", activated: {
params.openDocument(document)
})
case let .addScan(hasAny):
return FormControllerActionItem(type: .accent, title: hasAny ? "Upload More Scans" : "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 scans must contain proof of address."
}
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):
return FormControllerTextInputItem(title: "ID", text: value, placeholder: "ID", textUpdated: { text in
params.updateText(.identifier, text)
})
case let .firstName(value):
return FormControllerTextInputItem(title: "First Name", text: value, placeholder: "First Name", textUpdated: { text in
params.updateText(.firstName, text)
})
case let .lastName(value):
return FormControllerTextInputItem(title: "Last Name", text: value, placeholder: "Last Name", textUpdated: { text in
params.updateText(.lastName, text)
})
case let .gender(value):
var text = ""
if let value = value {
switch value {
case .male:
text = "Male"
case .female:
text = "Female"
}
}
return FormControllerDetailActionItem(title: "Gender", text: text, placeholder: "Gender", activated: {
params.activateSelection(.gender)
})
case let .countryCode(value):
return FormControllerDetailActionItem(title: "Country", text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: "Country", activated: {
params.activateSelection(.country)
})
case let .birthdate(value):
return FormControllerDetailActionItem(title: "Date of Birth", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Date of Birth", activated: {
params.activateSelection(.date(value?.timestamp, .birthdate))
})
case let .expiryDate(value):
return FormControllerDetailActionItem(title: "Expires", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Expires", activated: {
params.activateSelection(.date(value?.timestamp, .expiry))
})
case .deleteDocument:
return FormControllerActionItem(type: .destructive, title: "Delete Document", activated: {
params.deleteValue()
})
case let .street1(value):
return FormControllerTextInputItem(title: "Street 1", text: value, placeholder: "Street 1", textUpdated: { text in
params.updateText(.street1, text)
})
case let .street2(value):
return FormControllerTextInputItem(title: "Street 2", text: value, placeholder: "Street 2", textUpdated: { text in
params.updateText(.street2, text)
})
case let .city(value):
return FormControllerTextInputItem(title: "City", text: value, placeholder: "City", textUpdated: { text in
params.updateText(.city, text)
})
case let .state(value):
return FormControllerTextInputItem(title: "State", text: value, placeholder: "State", textUpdated: { text in
params.updateText(.state, text)
})
case let .postcode(value):
return FormControllerTextInputItem(title: "Postcode", text: value, placeholder: "Postcode", textUpdated: { text in
params.updateText(.postcode, text)
})
case .selfieHeader:
return FormControllerHeaderItem(text: "SELFIE")
case let .selfie(_, document):
return SecureIdValueFormFileItem(account: params.account, context: params.context, document: document, title: "Selfie", activated: {
params.openDocument(document)
})
case .addSelfie:
return FormControllerActionItem(type: .accent, title: "Upload Selfie", fullTopInset: true, activated: {
params.addFile(.selfie)
})
case .selfieInfo:
return FormControllerTextItem(text: "Take a selfie picture with youself holding the document.")
case let .error(_, text):
return FormControllerTextItem(text: text, color: .error)
}
}
}
struct SecureIdDocumentFormControllerNodeInitParams {
let account: Account
let context: SecureIdAccessContext
}
final class SecureIdDocumentFormControllerNode: FormControllerNode<SecureIdDocumentFormControllerNodeInitParams, SecureIdDocumentFormState> {
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)
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)
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))
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()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Male", action: {
dismissAction()
if let strongSelf = self, var innerState = strongSelf.innerState {
innerState.documentState.updateGenderField(type: .gender, value: .male)
strongSelf.updateInnerState(transition: .immediate, with: innerState)
}
}),
ActionSheetButtonItem(title: "Female", action: {
dismissAction()
if let strongSelf = self, var innerState = strongSelf.innerState {
innerState.documentState.updateGenderField(type: .gender, value: .female)
strongSelf.updateInnerState(transition: .immediate, with: innerState)
}
})
]),
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
}
}
strongSelf.updateInnerState(transition: .immediate, with: innerState)
}
}
}
deinit {
self.actionDisposable.dispose()
}
private func presentAssetPicker(_ type: AddFileTarget) {
let _ = legacyAssetPicker(theme: self.theme, fileMode: true, peer: nil, saveEditedPhotos: false, allowGrouping: false).start(next: { [weak self] generator in
if let strongSelf = self {
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.theme, initialLayout: strongSelf.layoutState?.layout)
legacyController.statusBar.statusBarStyle = strongSelf.theme.rootController.statusBar.style.style
let controller = generator(legacyController.context)
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
controller.captionsEnabled = false
controller.inhibitDocumentCaptions = true
controller.suggestionContext = nil
controller.dismissalBlock = {
}
controller.localMediaCacheEnabled = false
controller.shouldStoreAssets = false
controller.shouldShowFileTipIfNeeded = false
controller.descriptionGenerator = legacyAssetPickerItemGenerator()
controller.completionBlock = { [weak legacyController] signals in
if let strongSelf = self, let legacyController = legacyController {
legacyController.dismiss()
let _ = (legacyAssetPickerDataSignals(account: strongSelf.account, signals: signals!)
|> deliverOnMainQueue).start(next: { resources in
if let strongSelf = self {
strongSelf.addDocuments(type: type, resources: resources)
}
})
}
}
controller.dismissalBlock = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
strongSelf.view.endEditing(true)
strongSelf.present(legacyController, nil)
}
})
}
private func addDocuments(type: AddFileTarget, resources: [TelegramMediaResource]) {
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), 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), state: .uploading(0.0)))
break loop
}
}
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)
}
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<SecureIdValueWithContext, SaveSecureIdValueError>] = []
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(next: { [weak self] result in
if let strongSelf = self {
strongSelf.completedWithValues?([])
}
}, 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)
}
}))
}
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)))
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))))
if document.id == itemDocument.id {
centralIndex = index
}
index += 1
}
}
let galleryController = SecureIdDocumentGalleryController(account: self.account, context: self.context, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in
})
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, item.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 {
if item.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
}))
}
}