Swiftgram/TelegramUI/BotCheckoutInfoControllerNode.swift
Peter Iakovlev 47cd4b0d76 no message
2018-01-31 20:40:55 +04:00

444 lines
20 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
private final class BotCheckoutInfoAddressItems {
let address1: BotPaymentFieldItemNode
let address2: BotPaymentFieldItemNode
let city: BotPaymentFieldItemNode
let state: BotPaymentFieldItemNode
let country: BotPaymentDisclosureItemNode
let postcode: BotPaymentFieldItemNode
var items: [BotPaymentItemNode] {
return [
self.address1,
self.address2,
self.city,
self.state,
self.country,
self.postcode
]
}
init(strings: PresentationStrings, openCountrySelection: @escaping () -> Void) {
self.address1 = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoAddress1, placeholder: strings.CheckoutInfo_ShippingInfoAddress1Placeholder)
self.address2 = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoAddress2, placeholder: strings.CheckoutInfo_ShippingInfoAddress2Placeholder)
self.city = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoCity, placeholder: strings.CheckoutInfo_ShippingInfoCityPlaceholder)
self.state = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoState, placeholder: strings.CheckoutInfo_ShippingInfoStatePlaceholder)
self.country = BotPaymentDisclosureItemNode(title: strings.CheckoutInfo_ShippingInfoCountry, placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "")
self.postcode = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoPostcode, placeholder: strings.CheckoutInfo_ShippingInfoPostcodePlaceholder)
self.country.action = {
openCountrySelection()
}
}
}
private final class BotCheckoutInfoControllerScrollerNodeView: UIScrollView {
var ignoreUpdateBounds = false
override init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if !self.ignoreUpdateBounds {
super.bounds = value
}
}
}
override func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
}
}
private final class BotCheckoutInfoControllerScrollerNode: ASDisplayNode {
override var view: BotCheckoutInfoControllerScrollerNodeView {
return super.view as! BotCheckoutInfoControllerScrollerNodeView
}
override init() {
super.init()
self.setViewBlock({
return BotCheckoutInfoControllerScrollerNodeView(frame: CGRect())
})
}
}
enum BotCheckoutInfoControllerStatus {
case notReady
case ready
case verifying
}
final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let account: Account
private let invoice: BotPaymentInvoice
private let messageId: MessageId
private var focus: BotCheckoutInfoControllerFocus?
private let dismiss: () -> Void
private let openCountrySelection: () -> Void
private let updateStatus: (BotCheckoutInfoControllerStatus) -> Void
private let formInfoUpdated: (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void
private let present: (ViewController, Any?) -> Void
private var theme: PresentationTheme
private var strings: PresentationStrings
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let scrollNode: BotCheckoutInfoControllerScrollerNode
private let itemNodes: [[BotPaymentItemNode]]
private let addressItems: BotCheckoutInfoAddressItems?
private let nameItem: BotPaymentFieldItemNode?
private let emailItem: BotPaymentFieldItemNode?
private let phoneItem: BotPaymentFieldItemNode?
private let saveInfoItem: BotPaymentSwitchItemNode
private var formInfo: BotPaymentRequestedInfo
private let verifyDisposable = MetaDisposable()
private var isVerifying = false
init(account: Account, invoice: BotPaymentInvoice, messageId: MessageId, formInfo: BotPaymentRequestedInfo, focus: BotCheckoutInfoControllerFocus, theme: PresentationTheme, strings: PresentationStrings, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutInfoControllerStatus) -> Void, formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.account = account
self.invoice = invoice
self.messageId = messageId
self.formInfo = formInfo
self.focus = focus
self.dismiss = dismiss
self.openCountrySelection = openCountrySelection
self.updateStatus = updateStatus
self.formInfoUpdated = formInfoUpdated
self.present = present
self.theme = theme
self.strings = strings
self.scrollNode = BotCheckoutInfoControllerScrollerNode()
var itemNodes: [[BotPaymentItemNode]] = []
var openCountrySelectionImpl: (() -> Void)?
if invoice.requestedFields.contains(.shippingAddress) {
var sectionItems: [BotPaymentItemNode] = []
let addressItems = BotCheckoutInfoAddressItems(strings: strings, openCountrySelection: { openCountrySelectionImpl?()
})
addressItems.address1.text = formInfo.shippingAddress?.streetLine1 ?? ""
addressItems.address2.text = formInfo.shippingAddress?.streetLine2 ?? ""
addressItems.city.text = formInfo.shippingAddress?.city ?? ""
addressItems.state.text = formInfo.shippingAddress?.state ?? ""
if let iso2 = formInfo.shippingAddress?.countryIso2, let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2.uppercased()) {
addressItems.country.text = name
}
addressItems.postcode.text = formInfo.shippingAddress?.postCode ?? ""
sectionItems.append(BotPaymentHeaderItemNode(text: strings.CheckoutInfo_ShippingInfoTitle))
sectionItems.append(contentsOf: addressItems.items)
itemNodes.append(sectionItems)
self.addressItems = addressItems
} else {
self.addressItems = nil
}
if !invoice.requestedFields.intersection([.name, .phone, .email]).isEmpty {
var sectionItems: [BotPaymentItemNode] = []
sectionItems.append(BotPaymentHeaderItemNode(text: strings.CheckoutInfo_ReceiverInfoTitle))
if invoice.requestedFields.contains(.name) {
let nameItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoName, placeholder: strings.CheckoutInfo_ReceiverInfoNamePlaceholder)
nameItem.text = formInfo.name ?? ""
self.nameItem = nameItem
sectionItems.append(nameItem)
} else {
self.nameItem = nil
}
if invoice.requestedFields.contains(.email) {
let emailItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoEmail, placeholder: strings.CheckoutInfo_ReceiverInfoEmailPlaceholder)
emailItem.text = formInfo.email ?? ""
self.emailItem = emailItem
sectionItems.append(emailItem)
} else {
self.emailItem = nil
}
if invoice.requestedFields.contains(.phone) {
let phoneItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoPhone, placeholder: strings.CheckoutInfo_ReceiverInfoPhone)
phoneItem.text = formInfo.phone ?? ""
self.phoneItem = phoneItem
sectionItems.append(phoneItem)
} else {
self.phoneItem = nil
}
itemNodes.append(sectionItems)
} else {
self.nameItem = nil
self.emailItem = nil
self.phoneItem = nil
}
self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.CheckoutInfo_SaveInfo, isOn: true)
itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.CheckoutInfo_SaveInfoHelp)])
self.itemNodes = itemNodes
for items in itemNodes {
for item in items {
self.scrollNode.addSubnode(item)
}
}
super.init()
self.backgroundColor = self.theme.list.blocksBackgroundColor
self.scrollNode.backgroundColor = nil
self.scrollNode.isOpaque = false
self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.delegate = self
self.addSubnode(self.scrollNode)
openCountrySelectionImpl = { [weak self] in
if let strongSelf = self {
strongSelf.view.endEditing(true)
strongSelf.openCountrySelection()
}
}
for items in itemNodes {
for item in items {
if let item = item as? BotPaymentFieldItemNode {
item.textUpdated = { [weak self] in
self?.updateDone()
}
}
}
}
self.updateDone()
}
deinit {
self.verifyDisposable.dispose()
}
func updateCountry(_ iso2: String) {
if self.formInfo.shippingAddress?.countryIso2 != iso2, let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2) {
let shippingAddress: BotPaymentShippingAddress
if let current = self.formInfo.shippingAddress {
shippingAddress = current
} else {
shippingAddress = BotPaymentShippingAddress(streetLine1: "", streetLine2: "", city: "", state: "", countryIso2: iso2, postCode: "")
}
self.formInfo = BotPaymentRequestedInfo(name: self.formInfo.name, phone: self.formInfo.phone, email: self.formInfo.email, shippingAddress: BotPaymentShippingAddress(streetLine1: shippingAddress.streetLine1, streetLine2: shippingAddress.streetLine2, city: shippingAddress.city, state: shippingAddress.state, countryIso2: iso2, postCode: shippingAddress.postCode))
self.addressItems?.country.text = name
if let containerLayout = self.containerLayout {
self.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
}
self.updateDone()
}
}
private func collectFormInfo() -> BotPaymentRequestedInfo {
var address: BotPaymentShippingAddress?
if let addressItems = self.addressItems, let current = self.formInfo.shippingAddress {
address = BotPaymentShippingAddress(streetLine1: addressItems.address1.text, streetLine2: addressItems.address2.text, city: addressItems.city.text, state: addressItems.state.text, countryIso2: current.countryIso2, postCode: addressItems.postcode.text)
}
return BotPaymentRequestedInfo(name: self.nameItem?.text, phone: self.phoneItem?.text, email: self.emailItem?.text, shippingAddress: address)
}
func verify() {
self.isVerifying = true
let formInfo = self.collectFormInfo()
self.verifyDisposable.set((validateBotPaymentForm(network: self.account.network, saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.formInfoUpdated(formInfo, result)
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.isVerifying = false
strongSelf.updateDone()
let text: String
switch error {
case .shippingNotAvailable:
text = strongSelf.strings.CheckoutInfo_ErrorShippingNotAvailable
case .addressStateInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorStateInvalid
case .addressPostcodeInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorPostcodeInvalid
case .addressCityInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorCityInvalid
case .nameInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorNameInvalid
case .emailInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorEmailInvalid
case .phoneInvalid:
text = strongSelf.strings.CheckoutInfo_ErrorPhoneInvalid
case .generic:
text = strongSelf.strings.Login_UnknownError
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
}
}))
self.updateDone()
}
private func updateDone() {
var enabled = true
if let addressItems = self.addressItems {
if addressItems.address1.text.isEmpty {
enabled = false
}
if addressItems.city.text.isEmpty {
enabled = false
}
if let shippingAddress = self.formInfo.shippingAddress, shippingAddress.countryIso2.isEmpty {
enabled = false
}
if addressItems.postcode.text.isEmpty {
enabled = false
}
}
if let nameItem = self.nameItem, nameItem.text.isEmpty {
enabled = false
}
if let phoneItem = self.phoneItem, phoneItem.text.isEmpty {
enabled = false
}
if let emailItem = self.emailItem, emailItem.text.isEmpty {
enabled = false
}
if self.isVerifying {
self.updateStatus(.verifying)
} else if enabled {
self.updateStatus(.ready)
} else {
self.updateStatus(.notReady)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousLayout = self.containerLayout
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
var contentHeight: CGFloat = 0.0
var commonInset: CGFloat = 0.0
for items in self.itemNodes {
for item in items {
commonInset = max(commonInset, item.measureInset(theme: self.theme, width: layout.size.width))
}
}
for items in self.itemNodes {
if !items.isEmpty && items[0] is BotPaymentHeaderItemNode {
contentHeight += 24.0
} else {
contentHeight += 32.0
}
for i in 0 ..< items.count {
let item = items[i]
let itemHeight = item.updateLayout(theme: self.theme, width: layout.size.width, measuredInset: commonInset, previousItemNode: i == 0 ? nil : items[i - 1], nextItemNode: i == (items.count - 1) ? nil : items[i + 1], transition: transition)
transition.updateFrame(node: item, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: itemHeight)))
contentHeight += itemHeight
}
}
contentHeight += 24.0
let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight)
let previousBoundsOrigin = self.scrollNode.bounds.origin
self.scrollNode.view.ignoreUpdateBounds = true
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.scrollNode.view.contentSize = scrollContentSize
self.scrollNode.view.contentInset = insets
self.scrollNode.view.scrollIndicatorInsets = insets
self.scrollNode.view.ignoreUpdateBounds = false
if let focus = focus {
var focusItem: ASDisplayNode?
switch focus {
case .address:
focusItem = self.addressItems?.address1
case .name:
focusItem = self.nameItem
case .email:
focusItem = self.emailItem
case .phone:
focusItem = self.phoneItem
}
if let focusItem = focusItem {
let scrollVisibleSize = CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom)
var contentOffset = CGPoint(x: 0.0, y: -insets.top + floor(focusItem.frame.midY - scrollVisibleSize.height / 2.0))
contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height)
contentOffset.y = max(contentOffset.y, -insets.top)
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
if previousLayout == nil, let focusItem = focusItem as? BotPaymentFieldItemNode {
focusItem.activateInput()
}
}
} else if let previousLayout = previousLayout {
var previousInsets = previousLayout.0.insets(options: [.input])
previousInsets.top += max(previousLayout.1, previousLayout.0.insets(options: [.statusBar]).top)
let insetsScrollOffset = insets.top - previousInsets.top
var contentOffset = CGPoint(x: 0.0, y: previousBoundsOrigin.y + insetsScrollOffset)
contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height)
contentOffset.y = max(contentOffset.y, -insets.top)
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
} else {
let contentOffset = CGPoint(x: 0.0, y: -insets.top)
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
}
}
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.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: { [weak self] _ in
if let strongSelf = self {
strongSelf.dismiss()
}
completion?()
})
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.focus = nil
}
}