import Foundation import UIKit import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import ActivityIndicator import AccountContext final class SecureIdAuthControllerNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private let requestLayout: (ContainedViewLayoutTransition) -> Void private let interaction: SecureIdAuthControllerInteraction private var hapticFeedback: HapticFeedback? private var validLayout: (ContainerViewLayout, CGFloat)? private let activityIndicator: ActivityIndicator private let scrollNode: ASScrollNode private let headerNode: SecureIdAuthHeaderNode private var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? private var dismissedContentNode: (ASDisplayNode & SecureIdAuthContentNode)? private let acceptNode: SecureIdAuthAcceptNode private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? private var state: SecureIdAuthControllerState? private let deleteValueDisposable = MetaDisposable() init(context: AccountContext, presentationData: PresentationData, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) { self.context = context self.presentationData = presentationData self.requestLayout = requestLayout self.interaction = interaction self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.freeMonoIconColor, 22.0, 2.0, false)) self.activityIndicator.isHidden = true self.scrollNode = ASScrollNode() self.headerNode = SecureIdAuthHeaderNode(context: context, theme: presentationData.theme, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder) self.acceptNode = SecureIdAuthAcceptNode(title: presentationData.strings.Passport_Authorize, theme: presentationData.theme) super.init() self.addSubnode(self.activityIndicator) self.scrollNode.view.alwaysBounceVertical = true self.addSubnode(self.scrollNode) self.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.acceptNode.pressed = { [weak self] in guard let strongSelf = self, let state = strongSelf.state, case let .form(form) = state, let encryptedFormData = form.encryptedFormData, let formData = form.formData else { return } for (field, _, filled) in parseRequestedFormFields(formData.requestedFields, values: formData.values, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry) { if !filled { if let contentNode = strongSelf.contentNode as? SecureIdAuthFormContentNode { if let rect = contentNode.frameForField(field) { let subRect = contentNode.view.convert(rect, to: strongSelf.scrollNode.view) strongSelf.scrollNode.view.scrollRectToVisible(subRect, animated: true) } contentNode.highlightField(field) } if strongSelf.hapticFeedback == nil { strongSelf.hapticFeedback = HapticFeedback() } strongSelf.hapticFeedback?.error() return } } strongSelf.interaction.grant() } } deinit { self.deleteValueDisposable.dispose() } func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } func animateOut(completion: (() -> Void)? = nil) { self.isDisappearing = true self.view.endEditing(true) self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in completion?() }) } private var isDisappearing = false private var previousHeaderNodeAlpha: CGFloat = 0.0 private var hadContentNode = false func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight) if self.isDisappearing { return } let previousHadContentNode = self.hadContentNode self.hadContentNode = self.contentNode != nil var insetOptions: ContainerViewLayoutInsetOptions = [] if self.contentNode is SecureIdAuthPasswordOptionContentNode { insetOptions.insert(.input) } var insets = layout.insets(options: insetOptions) insets.bottom = max(insets.bottom, layout.safeInsets.bottom) let activitySize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - activitySize.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - activitySize.height) / 2.0)), size: activitySize)) var headerNodeTransition: ContainedViewLayoutTransition = self.headerNode.bounds.height.isZero ? .immediate : transition if self.previousHeaderNodeAlpha.isZero && !self.headerNode.alpha.isZero { headerNodeTransition = .immediate } self.previousHeaderNodeAlpha = self.headerNode.alpha let headerLayout: (compact: CGFloat, expanded: CGFloat, apply: (Bool) -> Void) if self.headerNode.alpha.isZero { headerLayout = (0.0, 0.0, { _ in }) } else { headerLayout = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition) } let acceptHeight = self.acceptNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition) var footerHeight: CGFloat = 0.0 var contentSpacing: CGFloat var acceptNodeTransition = transition if !previousHadContentNode { acceptNodeTransition = .immediate } acceptNodeTransition.updateFrame(node: self.acceptNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - acceptHeight), size: CGSize(width: layout.size.width, height: acceptHeight))) var minContentSpacing: CGFloat = 10.0 if self.acceptNode.supernode != nil { footerHeight += (acceptHeight - layout.intrinsicInsets.bottom) contentSpacing = 25.0 minContentSpacing = 25.0 } else { if self.contentNode is SecureIdAuthListContentNode { contentSpacing = 16.0 } else if self.contentNode is SecureIdAuthPasswordSetupContentNode { contentSpacing = 24.0 } else { contentSpacing = 56.0 } } insets.bottom += footerHeight let wrappingContentRect = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - insets.bottom - navigationBarHeight)) let contentRect = CGRect(origin: CGPoint(), size: wrappingContentRect.size) transition.updateFrame(node: self.scrollNode, frame: wrappingContentRect) if let contentNode = self.contentNode { let contentFirstTime = contentNode.bounds.isEmpty let contentNodeTransition: ContainedViewLayoutTransition = contentFirstTime ? .immediate : transition let contentLayout = contentNode.updateLayout(width: layout.size.width, transition: contentNodeTransition) let headerHeight: CGFloat if self.contentNode is SecureIdAuthPasswordOptionContentNode && headerLayout.expanded + contentLayout.height + minContentSpacing + 14.0 + 16.0 > contentRect.height { headerHeight = headerLayout.compact headerLayout.apply(false) } else { headerHeight = headerLayout.expanded headerLayout.apply(true) } contentSpacing = max(minContentSpacing, min(contentSpacing, contentRect.height - (headerHeight + contentLayout.height + minContentSpacing + 14.0 + 16.0))) let boundingHeight = headerHeight + contentLayout.height + contentSpacing var boundingRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: boundingHeight)) if contentNode is SecureIdAuthListContentNode { boundingRect.origin.y = contentRect.minY } else { boundingRect.origin.y = contentRect.minY + floor((contentRect.height - boundingHeight) / 2.0) } boundingRect.origin.y = max(boundingRect.origin.y, 14.0) if self.headerNode.alpha.isZero { headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: -boundingRect.width, y: self.headerNode.frame.minY), size: CGSize(width: boundingRect.width, height: headerHeight))) } else { headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY), size: CGSize(width: boundingRect.width, height: headerHeight))) } contentNodeTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY + headerHeight + contentSpacing), size: CGSize(width: boundingRect.width, height: contentLayout.height))) if contentFirstTime { contentNode.didAppear() if transition.isAnimated { contentNode.animateIn() if !(contentNode is SecureIdAuthPasswordOptionContentNode || contentNode is SecureIdAuthPasswordSetupContentNode) && previousHadContentNode { transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: layout.size.width, y: 0.0)) } } } self.scrollNode.view.contentSize = CGSize(width: boundingRect.width, height: 14.0 + boundingRect.height + 16.0) } if let dismissedContentNode = self.dismissedContentNode { self.dismissedContentNode = nil transition.updatePosition(node: dismissedContentNode, position: CGPoint(x: -layout.size.width / 2.0, y: dismissedContentNode.position.y), completion: { [weak dismissedContentNode] _ in dismissedContentNode?.removeFromSupernode() }) } } func transitionToContentNode(_ contentNode: (ASDisplayNode & SecureIdAuthContentNode)?, transition: ContainedViewLayoutTransition) { if let current = self.contentNode { current.willDisappear() if let dismissedContentNode = self.dismissedContentNode, dismissedContentNode !== current { dismissedContentNode.removeFromSupernode() } self.dismissedContentNode = current } self.contentNode = contentNode if let contentNode = self.contentNode { self.scrollNode.addSubnode(contentNode) if let _ = self.validLayout { if transition.isAnimated { self.scheduleLayoutTransitionRequest(.animated(duration: 0.5, curve: .spring)) } else { self.scheduleLayoutTransitionRequest(.immediate) } } } } func updateState(_ state: SecureIdAuthControllerState, transition: ContainedViewLayoutTransition) { self.state = state var displayActivity = false switch state { case let .form(form): if let encryptedFormData = form.encryptedFormData, let verificationState = form.verificationState { if self.headerNode.supernode == nil { self.scrollNode.addSubnode(self.headerNode) self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } self.headerNode.updateState(formData: encryptedFormData, verificationState: verificationState) var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? switch verificationState { case let .noChallenge(noChallengeState): if let _ = self.contentNode as? SecureIdAuthPasswordSetupContentNode { } else { let current = SecureIdAuthPasswordSetupContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, setupPassword: { [weak self] in self?.interaction.setupPassword() }) contentNode = current } switch noChallengeState { case .notSet: (self.contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(false) (contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(false) case .awaitingConfirmation: (self.contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(true) (contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(true) } case let .passwordChallenge(hint, challengeState, _): if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { current.updateIsChecking(challengeState == .checking) if case .invalid = challengeState { current.updateIsInvalid() } contentNode = current } else { let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in if let strongSelf = self { strongSelf.interaction.checkPassword(password) } }, passwordHelp: { [weak self] in self?.interaction.openPasswordHelp() }) current.updateIsChecking(challengeState == .checking) if case .invalid = challengeState { current.updateIsInvalid() } contentNode = current } case .verified: if let encryptedFormData = form.encryptedFormData, let formData = form.formData { if let current = self.contentNode as? SecureIdAuthFormContentNode { current.updateValues(formData.values) contentNode = current } else { let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in if let strongSelf = self { switch field { case .identity, .address: strongSelf.presentDocumentSelection(field: field) case .phone: strongSelf.presentPlaintextSelection(type: .phone) case .email: strongSelf.presentPlaintextSelection(type: .email) } } }, openURL: { [weak self] url in self?.interaction.openUrl(url) }, openMention: { [weak self] mention in self?.interaction.openMention(mention) }, requestLayout: { [weak self] in if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) } }) contentNode = current } } } if case .verified = verificationState { if self.acceptNode.supernode == nil { self.addSubnode(self.acceptNode) if transition.isAnimated { self.acceptNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.acceptNode.bounds.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } } if self.contentNode !== contentNode { self.transitionToContentNode(contentNode, transition: transition) } } else { displayActivity = true } case let .list(list): if let _ = list.encryptedValues, let verificationState = list.verificationState { if case .verified = verificationState { if !self.headerNode.alpha.isZero { self.headerNode.alpha = 0.0 self.headerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } } else { if self.headerNode.supernode == nil { self.scrollNode.addSubnode(self.headerNode) self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } self.headerNode.updateState(formData: nil, verificationState: verificationState) } var contentNode: (ASDisplayNode & SecureIdAuthContentNode)? switch verificationState { case let .passwordChallenge(hint, challengeState, _): if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { current.updateIsChecking(challengeState == .checking) if case .invalid = challengeState { current.updateIsInvalid() } contentNode = current } else { let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in self?.interaction.checkPassword(password) }, passwordHelp: { [weak self] in self?.interaction.openPasswordHelp() }) current.updateIsChecking(challengeState == .checking) if case .invalid = challengeState { current.updateIsInvalid() } contentNode = current } case .noChallenge: contentNode = nil case .verified: if let _ = list.encryptedValues, let values = list.values { if let current = self.contentNode as? SecureIdAuthListContentNode { current.updateValues(values) contentNode = current } else { let current = SecureIdAuthListContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, openField: { [weak self] field in self?.openListField(field) }, deleteAll: { [weak self] in self?.deleteAllValues() }, requestLayout: { [weak self] in if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) } }) current.updateValues(values) contentNode = current } } } if self.contentNode !== contentNode { self.transitionToContentNode(contentNode, transition: transition) } } else { displayActivity = true } } if displayActivity != !self.activityIndicator.isHidden { self.activityIndicator.isHidden = !displayActivity } } private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { let requestId = self.scheduledLayoutTransitionRequestId self.scheduledLayoutTransitionRequestId += 1 self.scheduledLayoutTransitionRequest = (requestId, transition) (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in if let strongSelf = self { if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { strongSelf.scheduledLayoutTransitionRequest = nil strongSelf.requestLayout(currentRequestTransition) } } }) self.setNeedsLayout() } private func presentDocumentSelection(field: SecureIdParsedRequestedFormField) { guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(secureIdContext) = verificationState, let encryptedFormData = form.encryptedFormData, let formData = form.formData else { return } let updatedValues: ([SecureIdValueKey], [SecureIdValueWithContext]) -> Void = { [weak self] touchedKeys, updatedValues in guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in guard let formData = form.formData, case let .form(form) = state else { return state } var values = formData.values.filter { value in return !touchedKeys.contains(value.value.key) } values.append(contentsOf: updatedValues) return .form(SecureIdAuthControllerFormState(twoStepEmail: form.twoStepEmail, encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues)) } } switch field { case let .identity(personalDetails, document): if let document = document { var hasValueType: (document: SecureIdRequestedIdentityDocument, requireSelfie: Bool, hasSelfie: Bool, requireTranslation: Bool, hasTranslation: Bool)? switch document { case let .just(type): if let value = findValue(formData.values, key: type.document.valueKey)?.1 { let data = extractSecureIdValueAdditionalData(value.value) switch value.value { case .passport: hasValueType = (.passport, type.selfie, data.selfie, type.translation, data.translation) case .idCard: hasValueType = (.idCard, type.selfie, data.selfie, type.translation, data.translation) case .driversLicense: hasValueType = (.driversLicense, type.selfie, data.selfie, type.translation, data.translation) case .internalPassport: hasValueType = (.internalPassport, type.selfie, data.selfie, type.translation, data.translation) default: break } } case let .oneOf(types): inner: for type in types.sorted(by: { $0.document.valueKey.rawValue < $1.document.valueKey.rawValue }) { if let value = findValue(formData.values, key: type.document.valueKey)?.1 { let data = extractSecureIdValueAdditionalData(value.value) var dataFilled = true if type.selfie && !data.selfie { dataFilled = false } if type.translation && !data.translation { dataFilled = false } if hasValueType == nil || dataFilled { switch value.value { case .passport: hasValueType = (.passport, type.selfie, data.selfie, type.translation, data.translation) case .idCard: hasValueType = (.idCard, type.selfie, data.selfie, type.translation, data.translation) case .driversLicense: hasValueType = (.driversLicense, type.selfie, data.selfie, type.translation, data.translation) case .internalPassport: hasValueType = (.internalPassport, type.selfie, data.selfie, type.translation, data.translation) default: break } if dataFilled { break inner } } } } } if let (hasValueType, requireSelfie, hasSelfie, requireTranslation, hasTranslation) = hasValueType { var scrollTo: SecureIdDocumentFormScrollToSubject? if requireSelfie && !hasSelfie { scrollTo = .selfie } else if requireTranslation && !hasTranslation { scrollTo = .translation } self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .identity(details: personalDetails, document: hasValueType, selfie: requireSelfie, translations: requireTranslation), scrollTo: scrollTo, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in var keys: [SecureIdValueKey] = [] if personalDetails != nil { keys.append(.personalDetails) } keys.append(hasValueType.valueKey) updatedValues(keys, values) })) return } } else if personalDetails != nil { self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .identity(details: personalDetails, document: nil, selfie: false, translations: false), primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in updatedValues([.personalDetails], values) })) return } case let .address(addressDetails, document): if let document = document { var hasValueType: (document: SecureIdRequestedAddressDocument, requireTranslation: Bool, hasTranslation: Bool)? switch document { case let .just(type): if let value = findValue(formData.values, key: type.document.valueKey)?.1 { let data = extractSecureIdValueAdditionalData(value.value) switch value.value { case .utilityBill: hasValueType = (.utilityBill, type.translation, data.translation) case .bankStatement: hasValueType = (.bankStatement, type.translation, data.translation) case .rentalAgreement: hasValueType = (.rentalAgreement, type.translation, data.translation) case .passportRegistration: hasValueType = (.passportRegistration, type.translation, data.translation) case .temporaryRegistration: hasValueType = (.temporaryRegistration, type.translation, data.translation) default: break } } case let .oneOf(types): inner: for type in types.sorted(by: { $0.document.valueKey.rawValue < $1.document.valueKey.rawValue }) { if let value = findValue(formData.values, key: type.document.valueKey)?.1 { let data = extractSecureIdValueAdditionalData(value.value) var dataFilled = true if type.translation && !data.translation { dataFilled = false } if hasValueType == nil || dataFilled { switch value.value { case .utilityBill: hasValueType = (.utilityBill, type.translation, data.translation) case .bankStatement: hasValueType = (.bankStatement, type.translation, data.translation) case .rentalAgreement: hasValueType = (.rentalAgreement, type.translation, data.translation) case .passportRegistration: hasValueType = (.passportRegistration, type.translation, data.translation) case .temporaryRegistration: hasValueType = (.temporaryRegistration, type.translation, data.translation) default: break } if dataFilled { break inner } } } } } if let (hasValueType, requireTranslation, hasTranslation) = hasValueType { var scrollTo: SecureIdDocumentFormScrollToSubject? if requireTranslation && !hasTranslation { scrollTo = .translation } self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .address(details: addressDetails, document: hasValueType, translations: requireTranslation), scrollTo: scrollTo, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in var keys: [SecureIdValueKey] = [] if addressDetails { keys.append(.address) } keys.append(hasValueType.valueKey) updatedValues(keys, values) })) return } } else if addressDetails { self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .address(details: addressDetails, document: nil, translations: false), primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in updatedValues([.address], values) })) return } default: break } let completionImpl: (SecureIdDocumentFormRequestedData) -> Void = { [weak self] requestedData in guard let strongSelf = self, let state = strongSelf.state, let verificationState = state.verificationState, case .verified = verificationState, let formData = form.formData, let validLayout = strongSelf.validLayout?.0 else { return } var attachmentType: SecureIdAttachmentMenuType? = nil var attachmentTarget: SecureIdAddFileTarget? = nil switch requestedData { case let .identity(_, document, _, _): if let document = document { switch document { case .idCard, .driversLicense: attachmentType = .idCard default: attachmentType = .generic } attachmentTarget = .frontSide(document) } case .address: attachmentType = .multiple attachmentTarget = .scan } let controller = SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: requestedData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in var keys: [SecureIdValueKey] = [] switch requestedData { case let .identity(details, document, _, _): if details != nil { keys.append(.personalDetails) } if let document = document { keys.append(document.valueKey) } case let .address(details, document, _): if details { keys.append(.address) } if let document = document { keys.append(document.valueKey) } } updatedValues(keys, values) }) if let attachmentType = attachmentType, let type = attachmentTarget { presentLegacySecureIdAttachmentMenu(context: strongSelf.context, present: { [weak self] c in self?.interaction.present(c, nil) }, validLayout: validLayout, type: attachmentType, recognizeDocumentData: true, completion: { [weak self] resources, recognizedData in guard let strongSelf = self else { return } strongSelf.interaction.present(controller, nil) controller.addDocuments(type: type, resources: resources, recognizedData: recognizedData, removeDocumentId: nil) }) } else { strongSelf.interaction.present(controller, nil) } } let itemsForField = documentSelectionItemsForField(field: field, strings: self.presentationData.strings) if itemsForField.count == 1 { completionImpl(itemsForField[0].1) } else { let controller = SecureIdDocumentTypeSelectionController(context: self.context, field: field, currentValues: formData.values, completion: completionImpl) self.interaction.present(controller, nil) } } private func presentPlaintextSelection(type: SecureIdPlaintextFormType) { guard let state = self.state, case let .form(form) = state, let formData = form.formData, let verificationState = form.verificationState, case let .verified(secureIdContext) = verificationState else { return } var immediatelyAvailableValue: SecureIdValue? var currentValue: SecureIdValueWithContext? switch type { case .phone: if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } currentValue = findValue(formData.values, key: .phone)?.1 case .email: if let email = form.twoStepEmail { immediatelyAvailableValue = .email(SecureIdEmailValue(email: email)) } currentValue = findValue(formData.values, key: .email)?.1 } let openForm: () -> Void = { [weak self] in guard let strongSelf = self else { return } strongSelf.interaction.push(SecureIdPlaintextFormController(context: strongSelf.context, secureIdContext: secureIdContext, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { valueWithContext in if let strongSelf = self { strongSelf.interaction.updateState { state in if case let .form(form) = state, let formData = form.formData { var values = formData.values switch type { case .phone: while let index = findValue(values, key: .phone)?.0 { values.remove(at: index) } case .email: while let index = findValue(values, key: .email)?.0 { values.remove(at: index) } } if let valueWithContext = valueWithContext { values.append(valueWithContext) } return .form(SecureIdAuthControllerFormState(twoStepEmail: form.twoStepEmail, encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues)) } return state } } })) } if let currentValue = currentValue { let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let text: String switch currentValue.value { case .phone: text = self.presentationData.strings.Passport_Phone_Delete default: text = self.presentationData.strings.Passport_Email_Delete } controller.setItemGroups([ ActionSheetItemGroup(items: [ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self] in dismissAction() guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in if case var .form(form) = state { form.removingValues = true return .form(form) } return state } strongSelf.deleteValueDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set([currentValue.value.key])) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in if case var .form(form) = state, let formData = form.formData { form.removingValues = false form.formData = SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: formData.values.filter { $0.value.key != currentValue.value.key }) return .form(form) } return state } })) })]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.interaction.present(controller, nil) } else { openForm() } } private func openListField(_ field: SecureIdAuthListContentField) { guard let state = self.state, case let .list(list) = state, let verificationState = list.verificationState, case let .verified(secureIdContext) = verificationState else { return } guard let values = list.values else { return } let updatedValues: (SecureIdValueKey) -> ([SecureIdValueWithContext]) -> Void = { valueKey in return { [weak self] updatedValues in guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in guard case var .list(list) = state, var values = list.values else { return state } values = values.filter({ value in return value.value.key != valueKey }) values.append(contentsOf: updatedValues) list.values = values return .list(list) } } } let openAction: (SecureIdValueKey) -> Void = { [weak self] field in guard let strongSelf = self, let state = strongSelf.state, case let .list(list) = state else { return } let primaryLanguageByCountry = list.primaryLanguageByCountry ?? [:] switch field { case .personalDetails: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: ParsedRequestedPersonalDetails(nativeNames: false), document: nil, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .passport: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .passport, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .internalPassport: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .internalPassport, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .driversLicense: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .driversLicense, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .idCard: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .idCard, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .address: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: true, document: nil, translations: false), primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .utilityBill: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .utilityBill, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .bankStatement: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .bankStatement, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .rentalAgreement: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .rentalAgreement, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .passportRegistration: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .passportRegistration, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .temporaryRegistration: strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .temporaryRegistration, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field))) case .phone: break case .email: break } } let deleteField: (SecureIdValueKey) -> Void = { [weak self] field in guard let strongSelf = self else { return } let controller = ActionSheetController(presentationData: strongSelf.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let text: String switch field { case .phone: text = strongSelf.presentationData.strings.Passport_Phone_Delete default: text = strongSelf.presentationData.strings.Passport_Email_Delete } controller.setItemGroups([ ActionSheetItemGroup(items: [ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self] in dismissAction() guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in if case var .list(list) = state { list.removingValues = true return .list(list) } return state } strongSelf.deleteValueDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set([field])) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } strongSelf.interaction.updateState { state in if case var .list(list) = state , let values = list.values { list.removingValues = false list.values = values.filter { $0.value.key != field } return .list(list) } return state } })) })]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.view.endEditing(true) strongSelf.interaction.present(controller, nil) } switch field { case .identity, .address: let keys: [(SecureIdValueKey, String, String)] let strings = self.presentationData.strings if case .identity = field { keys = [ (.personalDetails, strings.Passport_Identity_AddPersonalDetails, strings.Passport_Identity_EditPersonalDetails), (.passport, strings.Passport_Identity_AddPassport, strings.Passport_Identity_EditPassport), (.idCard, strings.Passport_Identity_AddIdentityCard, strings.Passport_Identity_EditIdentityCard), (.driversLicense, strings.Passport_Identity_AddDriversLicense, strings.Passport_Identity_EditDriversLicense), (.internalPassport, strings.Passport_Identity_AddInternalPassport, strings.Passport_Identity_EditInternalPassport), ] } else { keys = [ (.address, strings.Passport_Address_AddResidentialAddress, strings.Passport_Address_EditResidentialAddress), (.utilityBill, strings.Passport_Address_AddUtilityBill, strings.Passport_Address_EditUtilityBill), (.bankStatement, strings.Passport_Address_AddBankStatement, strings.Passport_Address_EditBankStatement), (.rentalAgreement, strings.Passport_Address_AddRentalAgreement, strings.Passport_Address_EditRentalAgreement), (.passportRegistration, strings.Passport_Address_AddPassportRegistration, strings.Passport_Address_EditPassportRegistration), (.temporaryRegistration, strings.Passport_Address_AddTemporaryRegistration, strings.Passport_Address_EditTemporaryRegistration) ] } let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var items: [ActionSheetItem] = [] for (key, add, edit) in keys { items.append(ActionSheetButtonItem(title: findValue(values, key: key) != nil ? edit : add, action: { dismissAction() openAction(key) })) } controller.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.interaction.present(controller, nil) case .phone: if findValue(values, key: .phone) != nil { deleteField(.phone) } else { var immediatelyAvailableValue: SecureIdValue? if let peer = list.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in updatedValues(.phone)(value.flatMap({ [$0] }) ?? []) })) } case .email: if findValue(values, key: .email) != nil { deleteField(.email) } else { var immediatelyAvailableValue: SecureIdValue? if let email = list.twoStepEmail { immediatelyAvailableValue = .email(SecureIdEmailValue(email: email)) } self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .email, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in updatedValues(.email)(value.flatMap({ [$0] }) ?? []) })) } } } private func deleteAllValues() { let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let items: [ActionSheetItem] = [ ActionSheetTextItem(title: self.presentationData.strings.Passport_DeletePassportConfirmation), ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, enabled: true, action: { [weak self] in dismissAction() self?.interaction.deleteAll() }) ] controller.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.interaction.present(controller, nil) } }