import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import Stripe import CountrySelectionUI private final class BotCheckoutNativeCardEntryScrollerNodeView: UIScrollView { var ignoreUpdateBounds = false 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 BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode { override var view: BotCheckoutNativeCardEntryScrollerNodeView { return super.view as! BotCheckoutNativeCardEntryScrollerNodeView } override init() { super.init() self.setViewBlock({ return BotCheckoutNativeCardEntryScrollerNodeView() }) } } final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let publishableKey: String private let present: (ViewController, Any?) -> Void private let dismiss: () -> Void private let openCountrySelection: () -> Void private let updateStatus: (BotCheckoutNativeCardEntryStatus) -> Void private let completion: (BotCheckoutPaymentMethod) -> Void private var theme: PresentationTheme private var strings: PresentationStrings private var containerLayout: (ContainerViewLayout, CGFloat)? private let scrollNode: BotCheckoutNativeCardEntryScrollerNode private let itemNodes: [[BotPaymentItemNode]] private let cardItem: BotPaymentCardInputItemNode private let cardholderItem: BotPaymentFieldItemNode? private let countryItem: BotPaymentDisclosureItemNode? private let zipCodeItem: BotPaymentFieldItemNode? private let saveInfoItem: BotPaymentSwitchItemNode private let verifyDisposable = MetaDisposable() private var isVerifying = false private var currentCardData: BotPaymentCardInputData? private var currentCountryIso2: String? init(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { self.publishableKey = publishableKey self.present = present self.dismiss = dismiss self.openCountrySelection = openCountrySelection self.updateStatus = updateStatus self.completion = completion self.theme = theme self.strings = strings self.scrollNode = BotCheckoutNativeCardEntryScrollerNode() var itemNodes: [[BotPaymentItemNode]] = [] var cardUpdatedImpl: ((BotPaymentCardInputData?) -> Void)? var openCountrySelectionImpl: (() -> Void)? self.cardItem = BotPaymentCardInputItemNode() cardItem.updated = { data in cardUpdatedImpl?(data) } itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem]) if additionalFields.contains(.cardholderName) { var sectionItems: [BotPaymentItemNode] = [] sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) self.cardholderItem = cardholderItem sectionItems.append(cardholderItem) itemNodes.append(sectionItems) } else { self.cardholderItem = nil } if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { var sectionItems: [BotPaymentItemNode] = [] sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) if additionalFields.contains(.country) { let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") countryItem.action = { openCountrySelectionImpl?() } self.countryItem = countryItem sectionItems.append(countryItem) } else { self.countryItem = nil } if additionalFields.contains(.zipCode) { let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address) self.zipCodeItem = zipCodeItem sectionItems.append(zipCodeItem) } else { self.zipCodeItem = nil } itemNodes.append(sectionItems) } else { self.countryItem = nil self.zipCodeItem = nil } self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: false) itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_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 if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.alwaysBounceVertical = true self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.delegate = self self.addSubnode(self.scrollNode) cardUpdatedImpl = { [weak self] data in if let strongSelf = self { strongSelf.currentCardData = data strongSelf.updateDone() } } 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() } item.returnPressed = { [weak self, weak item] in guard let strongSelf = self, let item = item else { return } var activateNext = true 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 { field.activateInput() break outer } } } } } } } cardItem.completed = { [weak self] in self?.cardholderItem?.activateInput() } self.updateDone() } deinit { self.verifyDisposable.dispose() } func updateCountry(_ iso2: String) { if let name = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(iso2, strings: self.strings) { self.currentCountryIso2 = iso2 self.countryItem?.text = name if let containerLayout = self.containerLayout { self.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.updateDone() } } func verify() { guard let cardData = self.currentCardData else { return } let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration configuration.smsAutofillDisabled = true configuration.publishableKey = self.publishableKey configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" let apiClient = STPAPIClient(configuration: configuration) let card = STPCardParams() card.number = cardData.number card.cvc = cardData.code card.expYear = cardData.year card.expMonth = cardData.month card.name = self.cardholderItem?.text card.addressCountry = self.currentCountryIso2 card.addressZip = self.zipCodeItem?.text let createToken: Signal = Signal { subscriber in apiClient.createToken(withCard: card, completion: { token, error in if let error = error { subscriber.putError(error) } else if let token = token { subscriber.putNext(token) subscriber.putCompletion() } }) return ActionDisposable { let _ = apiClient.publishableKey } } self.isVerifying = true self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self, let card = token.card { let last4 = card.last4() let brand = STPAPIClient.string(with: card.brand) strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn))) } }, error: { [weak self] error in if let strongSelf = self { strongSelf.isVerifying = false strongSelf.updateDone() } })) self.updateDone() } private func updateDone() { var enabled = true if self.currentCardData == nil { enabled = false } if let cardholderItem = self.cardholderItem, cardholderItem.text.isEmpty { enabled = false } if let _ = self.countryItem, self.currentCountryIso2 == nil { enabled = false } if let zipCodeItem = self.zipCodeItem, zipCodeItem.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 if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) } if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize } if self.scrollNode.view.contentInset != insets { self.scrollNode.view.contentInset = insets } if self.scrollNode.view.scrollIndicatorInsets != insets { self.scrollNode.view.scrollIndicatorInsets = insets } self.scrollNode.view.ignoreUpdateBounds = false 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 activate() { self.cardItem.activateInput() } }