Swiftgram/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift
2023-04-20 19:22:25 +04:00

981 lines
58 KiB
Swift

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: EnginePeer(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)
}
}