import Foundation import Display import AsyncDisplayKit import Postbox import TelegramCore final class SecureIdAuthControllerNode: ViewControllerTracingNode { private let account: Account private var presentationData: PresentationData private let requestLayout: (ContainedViewLayoutTransition) -> Void private let interaction: SecureIdAuthControllerInteraction private var validLayout: (ContainerViewLayout, CGFloat)? 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? init(account: Account, presentationData: PresentationData, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) { self.account = account self.presentationData = presentationData self.requestLayout = requestLayout self.interaction = interaction self.scrollNode = ASScrollNode() self.headerNode = SecureIdAuthHeaderNode(account: account, theme: presentationData.theme, strings: presentationData.strings) self.acceptNode = SecureIdAuthAcceptNode(title: "Authorize", theme: presentationData.theme) super.init() self.scrollNode.view.alwaysBounceVertical = true self.addSubnode(self.scrollNode) self.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.acceptNode.pressed = { [weak self] in self?.interaction.grant() } } 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.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: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in completion?() }) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.input]) insets.bottom = max(insets.bottom, layout.safeInsets.bottom) let headerNodeTransition: ContainedViewLayoutTransition = headerNode.bounds.isEmpty ? .immediate : transition let headerHeight: CGFloat if self.headerNode.alpha.isZero { headerHeight = 0.0 } else { headerHeight = 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 transition.updateFrame(node: self.acceptNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - acceptHeight), size: CGSize(width: layout.size.width, height: acceptHeight))) if self.acceptNode.supernode != nil { footerHeight += acceptHeight contentSpacing = 25.0 } else { if self.contentNode is SecureIdAuthListContentNode { contentSpacing = 16.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) let overscrollY = self.scrollNode.view.bounds.minY 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 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) { 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 { self.scheduleLayoutTransitionRequest(.animated(duration: 0.5, curve: .spring)) } } } func updateState(_ state: SecureIdAuthControllerState, transition: ContainedViewLayoutTransition) { self.state = state 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 .passwordChallenge(hint, challengeState): if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode { current.updateIsChecking(challengeState == .checking) 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 if let strongSelf = self { } }) current.updateIsChecking(challengeState == .checking) contentNode = current } case .noChallenge: contentNode = nil 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, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, 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) }) contentNode = current } } } if case .verified = verificationState { if self.acceptNode.supernode == nil { self.addSubnode(self.acceptNode) 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) } } 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) 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 if let strongSelf = self { } }) current.updateIsChecking(challengeState == .checking) 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, openField: { [weak self] field in self?.openListField(field) }, deleteAll: { [weak self] in self?.deleteAllValues() }) current.updateValues(values) contentNode = current } } } if self.contentNode !== contentNode { self.transitionToContentNode(contentNode, transition: transition) } } } } 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(context) = verificationState, let formData = form.formData else { return } let updatedValue: ([SecureIdValueWithContext]) -> Void = { [weak self] updatedValues in if let strongSelf = self { strongSelf.interaction.updateState { state in switch state { case let .form(form): if let formData = form.formData { var values = formData.values.filter { value in switch field { case let .identity(personalDetails, document, _): if personalDetails { if case .personalDetails = value.value.key { return false } } switch value.value.key { case .passport: if document.contains(.passport) { return false } case .driversLicense: if document.contains(.driversLicense) { return false } case .idCard: if document.contains(.idCard) { return false } default: break } case let .address(addressDetails, document): if addressDetails { if case .address = value.value.key { return false } } switch value.value.key { case .bankStatement: if document.contains(.bankStatement) { return false } case .utilityBill: if document.contains(.utilityBill) { return false } case .rentalAgreement: if document.contains(.rentalAgreement) { return false } default: break } case .phone: break case .email: break } return true } values.append(contentsOf: updatedValues) return .form(SecureIdAuthControllerFormState(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState)) } case let .list(list): break } return state } } } switch field { case let .identity(personalDetails, document, selfie): var hasPersonalDetails = !personalDetails if personalDetails { if findValue(formData.values, key: .personalDetails) != nil { hasPersonalDetails = true } } var hasValueType: SecureIdRequestedIdentityDocument? loop: for documentType in document { switch documentType { case .passport: if findValue(formData.values, key: .passport) != nil { hasValueType = .passport break loop } case .internalPassport: if findValue(formData.values, key: .internalPassport) != nil { hasValueType = .internalPassport break loop } case .driversLicense: if findValue(formData.values, key: .driversLicense) != nil { hasValueType = .driversLicense break loop } case .idCard: if findValue(formData.values, key: .idCard) != nil { hasValueType = .idCard break loop } } } if hasValueType != nil || hasPersonalDetails { self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .identity(details: personalDetails, document: hasValueType, selfie: selfie), values: formData.values, updatedValues: updatedValue), nil) return } case let .address(addressDetails, document): var hasValueType: SecureIdRequestedAddressDocument? loop: for documentType in document { switch documentType { case .passportRegistration: if findValue(formData.values, key: .passportRegistration) != nil { hasValueType = .passportRegistration break loop } case .temporaryRegistration: if findValue(formData.values, key: .temporaryRegistration) != nil { hasValueType = .temporaryRegistration break loop } case .bankStatement: if findValue(formData.values, key: .bankStatement) != nil { hasValueType = .bankStatement break loop } case .utilityBill: if findValue(formData.values, key: .utilityBill) != nil { hasValueType = .utilityBill break loop } case .rentalAgreement: if findValue(formData.values, key: .rentalAgreement) != nil { hasValueType = .rentalAgreement break loop } } } if let hasValueType = hasValueType { self.interaction.present(SecureIdDocumentFormController(account: self.account, context: context, requestedData: .address(details: addressDetails, document: hasValueType), values: formData.values, updatedValues: updatedValue), nil) return } default: break } let controller = SecureIdDocumentTypeSelectionController(theme: self.presentationData.theme, strings: self.presentationData.strings, field: field, currentValues: formData.values, completion: { [weak self] requestedData in guard let strongSelf = self, let state = strongSelf.state, let verificationState = state.verificationState, case let .verified(context) = verificationState, let formData = form.formData else { return } strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: requestedData, values: formData.values, updatedValues: updatedValue), nil) }) self.interaction.present(controller, nil) } private func presentPlaintextSelection(type: SecureIdPlaintextFormType) { guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(context) = verificationState, let formData = form.formData else { return } var immediatelyAvailableValue: SecureIdValue? switch type { case .phone: if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } default: break } self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { [weak self] 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(encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState)) } return state } } }), nil) } private func openListField(_ field: SecureIdAuthListContentField) { guard let state = self.state, case let .list(list) = state, let verificationState = list.verificationState, case let .verified(context) = 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 else { return } switch field { case .personalDetails: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: true, document: nil, selfie: false), values: values, updatedValues: updatedValues(field)), nil) case .passport: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .passport, selfie: true), values: values, updatedValues: updatedValues(field)), nil) case .internalPassport: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .internalPassport, selfie: true), values: values, updatedValues: updatedValues(field)), nil) case .driversLicense: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .driversLicense, selfie: true), values: values, updatedValues: updatedValues(field)), nil) case .idCard: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .identity(details: false, document: .idCard, selfie: true), values: values, updatedValues: updatedValues(field)), nil) case .passportRegistration: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .passportRegistration), values: values, updatedValues: updatedValues(field)), nil) case .temporaryRegistration: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .temporaryRegistration), values: values, updatedValues: updatedValues(field)), nil) case .address: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: true, document: nil), values: values, updatedValues: updatedValues(field)), nil) case .utilityBill: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .utilityBill), values: values, updatedValues: updatedValues(field)), nil) case .bankStatement: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .bankStatement), values: values, updatedValues: updatedValues(field)), nil) case .rentalAgreement: strongSelf.interaction.present(SecureIdDocumentFormController(account: strongSelf.account, context: context, requestedData: .address(details: false, document: .rentalAgreement), values: values, updatedValues: updatedValues(field)), nil) case .phone: break case .email: break } } switch field { case .identity, .address: let keys: [(SecureIdValueKey, String, String)] if case .identity = field { keys = [ (.personalDetails, "Add Personal Details", "Edit Personal Details"), (.passport, "Add Passport", "Edit Passport"), (.idCard, "Add Identity Card", "Edit Identity Card"), (.driversLicense, "Add Driver's License", "Edit Driver's License"), (.internalPassport, "Add Internal Passport", "Edit Internal Passport"), ] } else { keys = [ (.address, "Add Residential Address", "Edit Residential Address"), (.utilityBill, "Add Utility Bill", "Edit Utility Bill"), (.bankStatement, "Add Bank Statement", "Edit Bank Statement"), (.rentalAgreement, "Add Rental Agreement", "Edit Rental Agreement"), (.passportRegistration, "Add Passport Registration", "Edit Passport Registration"), (.temporaryRegistration, "Add Temporary Registration", "Edit Temporary Registration") ] } let controller = ActionSheetController(presentationTheme: self.presentationData.theme) 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: var immediatelyAvailableValue: SecureIdValue? self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in updatedValues(.phone)(value.flatMap({ [$0] }) ?? []) }), nil) case .email: self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: .email, immediatelyAvailableValue: nil, updatedValue: { value in updatedValues(.email)(value.flatMap({ [$0] }) ?? []) }), nil) } } private func deleteAllValues() { let controller = ActionSheetController(presentationTheme: self.presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let items: [ActionSheetItem] = [ ActionSheetTextItem(title: "Are you sure you want to delete your Telegram Passport? All details will be lost."), 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) } }