Swiftgram/TelegramUI/SecureIdAuthControllerNode.swift
2018-06-16 20:03:02 +03:00

643 lines
37 KiB
Swift

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)
}
}