Swiftgram/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift
2019-12-08 02:52:11 +04:00

983 lines
58 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
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)
}
}