Swiftgram/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift
2021-03-26 18:33:46 +04:00

521 lines
24 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AlertUI
import PresentationDataUtils
import CountrySelectionUI
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, contentType: .address)
self.address2 = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoAddress2, placeholder: strings.CheckoutInfo_ShippingInfoAddress2Placeholder, contentType: .address)
self.city = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoCity, placeholder: strings.CheckoutInfo_ShippingInfoCityPlaceholder, contentType: .address)
self.state = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoState, placeholder: strings.CheckoutInfo_ShippingInfoStatePlaceholder, contentType: .address)
self.country = BotPaymentDisclosureItemNode(title: strings.CheckoutInfo_ShippingInfoCountry, placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "")
self.postcode = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ShippingInfoPostcode, placeholder: strings.CheckoutInfo_ShippingInfoPostcodePlaceholder, contentType: .address)
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, iOS 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 context: AccountContext
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(context: AccountContext, 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.context = context
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(), strings: self.strings) {
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, contentType: .name)
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, contentType: .email)
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, contentType: .phoneNumber)
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()
}
}
let fieldsAndTypes = { [weak self] () -> [(BotPaymentFieldItemNode, BotCheckoutInfoControllerFocus)] in
guard let strongSelf = self else {
return []
}
var fieldsAndTypes: [(BotPaymentFieldItemNode, BotCheckoutInfoControllerFocus)] = []
if let addressItems = strongSelf.addressItems {
fieldsAndTypes.append((addressItems.address1, .address(.street1)))
fieldsAndTypes.append((addressItems.address2, .address(.street2)))
fieldsAndTypes.append((addressItems.city, .address(.city)))
fieldsAndTypes.append((addressItems.state, .address(.state)))
fieldsAndTypes.append((addressItems.postcode, .address(.postcode)))
}
if let nameItem = strongSelf.nameItem {
fieldsAndTypes.append((nameItem, .name))
}
if let phoneItem = strongSelf.phoneItem {
fieldsAndTypes.append((phoneItem, .phone))
}
if let emailItem = strongSelf.emailItem {
fieldsAndTypes.append((emailItem, .email))
}
return fieldsAndTypes
}
for items in itemNodes {
for item in items {
if let item = item as? BotPaymentFieldItemNode {
item.focused = { [weak self, weak item] in
guard let strongSelf = self, let item = item else {
return
}
for (node, focus) in fieldsAndTypes() {
if node === item {
strongSelf.focus = focus
break
}
}
}
item.textUpdated = { [weak self] in
self?.updateDone()
}
item.returnPressed = { [weak self, weak item] in
guard let strongSelf = self, let item = item else {
return
}
var activateNext = false
outer: for section in strongSelf.itemNodes {
for i in 0 ..< section.count {
if section[i] === item {
activateNext = true
} else if activateNext, let field = section[i] as? BotPaymentFieldItemNode {
for (node, focus) in fieldsAndTypes() {
if node === field {
strongSelf.focus = focus
if let containerLayout = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
}
break outer
}
}
}
}
}
}
}
}
}
self.updateDone()
}
deinit {
self.verifyDisposable.dispose()
}
func updateCountry(_ iso2: String) {
if self.formInfo.shippingAddress?.countryIso2 != iso2, let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2, strings: self.strings) {
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), tipAmount: self.formInfo.tipAmount)
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, tipAmount: self.formInfo.tipAmount)
}
func verify() {
self.isVerifying = true
let formInfo = self.collectFormInfo()
self.verifyDisposable.set((validateBotPaymentForm(account: self.context.account, 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(textAlertController(context: strongSelf.context, 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 = self.focus {
var focusItem: ASDisplayNode?
switch focus {
case let .address(field):
switch field {
case .street1:
focusItem = self.addressItems?.address1
case .street2:
focusItem = self.addressItems?.address2
case .city:
focusItem = self.addressItems?.city
case .state:
focusItem = self.addressItems?.state
case .postcode:
focusItem = self.addressItems?.postcode
}
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 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: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.dismiss()
}
completion?()
})
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.focus = nil
}
}