mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Refactor BotPaymentsUI and LocalAuth
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
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<STPToken, Error> = 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user