mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Initial tip support
This commit is contained in:
@@ -24,14 +24,14 @@ final class BotCheckoutControllerArguments {
|
||||
fileprivate let openInfo: (BotCheckoutInfoControllerFocus) -> Void
|
||||
fileprivate let openPaymentMethod: () -> Void
|
||||
fileprivate let openShippingMethod: () -> Void
|
||||
fileprivate let openTip: () -> Void
|
||||
fileprivate let updateTip: (Int64) -> Void
|
||||
|
||||
fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, openTip: @escaping () -> Void) {
|
||||
fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, updateTip: @escaping (Int64) -> Void) {
|
||||
self.account = account
|
||||
self.openInfo = openInfo
|
||||
self.openPaymentMethod = openPaymentMethod
|
||||
self.openShippingMethod = openShippingMethod
|
||||
self.openTip = openTip
|
||||
self.updateTip = updateTip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ private enum BotCheckoutSection: Int32 {
|
||||
|
||||
enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
case header(PresentationTheme, TelegramMediaInvoice, String)
|
||||
case price(Int, PresentationTheme, String, String, Bool)
|
||||
case tip(PresentationTheme, String, String)
|
||||
case price(Int, PresentationTheme, String, String, Bool, Bool)
|
||||
case tip(Int, PresentationTheme, String, String, String, Int64, [(String, Int64)])
|
||||
case paymentMethod(PresentationTheme, String, String)
|
||||
case shippingInfo(PresentationTheme, String, String)
|
||||
case shippingMethod(PresentationTheme, String, String)
|
||||
@@ -56,7 +56,7 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .header:
|
||||
return BotCheckoutSection.header.rawValue
|
||||
case .price:
|
||||
case .price, .tip:
|
||||
return BotCheckoutSection.prices.rawValue
|
||||
default:
|
||||
return BotCheckoutSection.info.rawValue
|
||||
@@ -67,10 +67,10 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .header:
|
||||
return 0
|
||||
case let .price(index, _, _, _, _):
|
||||
case let .price(index, _, _, _, _, _):
|
||||
return 1 + Int32(index)
|
||||
case let .tip(index, _, _, _, _, _, _):
|
||||
return 1 + Int32(index)
|
||||
case .tip:
|
||||
return 10000 + 1
|
||||
case .paymentMethod:
|
||||
return 10000 + 2
|
||||
case .shippingInfo:
|
||||
@@ -103,8 +103,8 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal):
|
||||
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs {
|
||||
case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator):
|
||||
if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
@@ -120,12 +120,26 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
if lhsFinal != rhsFinal {
|
||||
return false
|
||||
}
|
||||
if lhsHasSeparator != rhsHasSeparator {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .tip(lhsTheme, lhsText, lhsValue):
|
||||
if case let .tip(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .tip(lhsIndex, lhsTheme, lhsText, lhsCurrency, lhsValue, lhsNumericValue, lhsVariants):
|
||||
if case let .tip(rhsIndex, rhsTheme, rhsText, rhsCurrency, rhsValue, rhsNumericValue, rhsVariants) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsCurrency == rhsCurrency, lhsValue == rhsValue, lhsNumericValue == rhsNumericValue {
|
||||
if lhsVariants.count != rhsVariants.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhsVariants.count {
|
||||
if lhsVariants[i].0 != rhsVariants[i].0 {
|
||||
return false
|
||||
}
|
||||
if lhsVariants[i].1 != rhsVariants[i].1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -178,11 +192,11 @@ enum BotCheckoutEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case let .header(theme, invoice, botName):
|
||||
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
|
||||
case let .price(_, theme, text, value, isFinal):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section)
|
||||
case let .tip(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openTip()
|
||||
case let .price(_, theme, text, value, isFinal, hasSeparator):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, sectionId: self.section)
|
||||
case let .tip(_, _, text, currency, value, numericValue, variants):
|
||||
return BotCheckoutTipItem(theme: presentationData.theme, title: text, currency: currency, value: value, numericValue: numericValue, availableVariants: variants, sectionId: self.section, updateValue: { value in
|
||||
arguments.updateTip(value)
|
||||
})
|
||||
case let .paymentMethod(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
@@ -272,7 +286,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
|
||||
|
||||
var index = 0
|
||||
for price in paymentForm.invoice.prices {
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false))
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false))
|
||||
totalPrice += price.amount
|
||||
index += 1
|
||||
}
|
||||
@@ -286,7 +300,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
|
||||
shippingOptionString = option.title
|
||||
|
||||
for price in option.prices {
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false))
|
||||
entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false))
|
||||
totalPrice += price.amount
|
||||
index += 1
|
||||
}
|
||||
@@ -296,16 +310,28 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true))
|
||||
|
||||
if !entries.isEmpty {
|
||||
switch entries[entries.count - 1] {
|
||||
case let .price(index, theme, title, value, _, _):
|
||||
entries[entries.count - 1] = .price(index, theme, title, value, false, false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let tip = paymentForm.invoice.tip {
|
||||
let tipTitle: String
|
||||
//TODO:localize
|
||||
tipTitle = "Tip"
|
||||
entries.append(.tip(presentationData.theme, tipTitle, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))"))
|
||||
tipTitle = "Tip (Optional)"
|
||||
entries.append(.tip(index, presentationData.theme, tipTitle, paymentForm.invoice.currency, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))", currentTip ?? 0, tip.suggested.map { item -> (String, Int64) in
|
||||
return ("\(formatCurrencyAmount(item, currency: paymentForm.invoice.currency))", item)
|
||||
}))
|
||||
index += 1
|
||||
}
|
||||
|
||||
entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true, true))
|
||||
|
||||
var paymentMethodTitle = ""
|
||||
if let currentPaymentMethod = currentPaymentMethod {
|
||||
paymentMethodTitle = currentPaymentMethod.title
|
||||
@@ -439,7 +465,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var openInfoImpl: ((BotCheckoutInfoControllerFocus) -> Void)?
|
||||
var openTipImpl: (() -> Void)?
|
||||
var updateTipImpl: ((Int64) -> Void)?
|
||||
var openPaymentMethodImpl: (() -> Void)?
|
||||
var openShippingMethodImpl: (() -> Void)?
|
||||
|
||||
@@ -449,8 +475,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
openPaymentMethodImpl?()
|
||||
}, openShippingMethod: {
|
||||
openShippingMethodImpl?()
|
||||
}, openTip: {
|
||||
openTipImpl?()
|
||||
}, updateTip: { value in
|
||||
updateTipImpl?(value)
|
||||
})
|
||||
|
||||
let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), context.account.postbox.loadedPeerWithId(messageId.peerId))
|
||||
@@ -643,30 +669,20 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
}
|
||||
}
|
||||
|
||||
openTipImpl = { [weak self] in
|
||||
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue {
|
||||
//TODO:localize
|
||||
let initialValue: String
|
||||
if let tipAmount = strongSelf.currentTipAmount, let value = currencyToFractionalAmount(value: tipAmount, currency: paymentFormValue.invoice.currency) {
|
||||
initialValue = "\(value)"
|
||||
} else {
|
||||
initialValue = "0"
|
||||
}
|
||||
let controller = tipEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: nil, title: "Tip", text: "Enter Tip Amount", placeholder: "", value: initialValue, apply: { value in
|
||||
guard let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo, let value = value else {
|
||||
return
|
||||
}
|
||||
|
||||
let tipAmount = fractionalToCurrencyAmount(value: (Double(value) ?? 0.0), currency: paymentFormValue.invoice.currency) ?? 0
|
||||
|
||||
strongSelf.currentTipAmount = tipAmount
|
||||
|
||||
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
|
||||
|
||||
strongSelf.updateActionButton()
|
||||
})
|
||||
strongSelf.present(controller, nil)
|
||||
updateTipImpl = { [weak self] value in
|
||||
guard let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo else {
|
||||
return
|
||||
}
|
||||
|
||||
if strongSelf.currentTipAmount == value {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.currentTipAmount = value
|
||||
|
||||
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
|
||||
|
||||
strongSelf.updateActionButton()
|
||||
}
|
||||
|
||||
openPaymentMethodImpl = { [weak self] in
|
||||
|
||||
@@ -12,15 +12,17 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem {
|
||||
let title: String
|
||||
let label: String
|
||||
let isFinal: Bool
|
||||
let hasSeparator: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
let requestsNoInset: Bool = true
|
||||
|
||||
init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, sectionId: ItemListSectionId) {
|
||||
init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, hasSeparator: Bool, sectionId: ItemListSectionId) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.isFinal = isFinal
|
||||
self.hasSeparator = hasSeparator
|
||||
self.sectionId = sectionId
|
||||
}
|
||||
|
||||
@@ -83,6 +85,10 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
|
||||
let separatorNode: ASDisplayNode
|
||||
let bottomSeparatorNode: ASDisplayNode
|
||||
let spacerNode: ASDisplayNode
|
||||
|
||||
private var item: BotCheckoutPriceItem?
|
||||
|
||||
@@ -92,11 +98,18 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.bottomSeparatorNode = ASDisplayNode()
|
||||
self.spacerNode = ASDisplayNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
|
||||
self.addSubnode(self.spacerNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.bottomSeparatorNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
@@ -105,9 +118,18 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
|
||||
return { item, params, neighbors in
|
||||
let rightInset: CGFloat = 16.0 + params.rightInset
|
||||
|
||||
let naturalContentHeight: CGFloat = 34.0
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: 34.0)
|
||||
let insets = priceItemInsets(neighbors)
|
||||
var contentSize = CGSize(width: params.width, height: naturalContentHeight)
|
||||
var insets = priceItemInsets(neighbors)
|
||||
|
||||
if item.hasSeparator {
|
||||
insets.top += 5.0
|
||||
}
|
||||
if item.isFinal {
|
||||
contentSize.height += 34.0
|
||||
}
|
||||
|
||||
let textFont: UIFont
|
||||
let textColor: UIColor
|
||||
@@ -130,9 +152,26 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
|
||||
let _ = labelApply()
|
||||
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
|
||||
strongSelf.separatorNode.isHidden = !item.hasSeparator
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
|
||||
|
||||
strongSelf.bottomSeparatorNode.isHidden = !item.isFinal
|
||||
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
|
||||
strongSelf.spacerNode.isHidden = !item.isFinal
|
||||
strongSelf.spacerNode.backgroundColor = item.theme.list.blocksBackgroundColor
|
||||
strongSelf.spacerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: naturalContentHeight + 10.0 + UIScreenPixel), size: CGSize(width: params.width, height: max(0.0, contentSize.height - naturalContentHeight - UIScreenPixel)))
|
||||
|
||||
var verticalOffset: CGFloat = 0.0
|
||||
if item.hasSeparator {
|
||||
verticalOffset += 5.0
|
||||
}
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
355
submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift
Normal file
355
submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift
Normal file
@@ -0,0 +1,355 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import TelegramStringFormatting
|
||||
|
||||
class BotCheckoutTipItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let currency: String
|
||||
let value: String
|
||||
let numericValue: Int64
|
||||
let availableVariants: [(String, Int64)]
|
||||
let updateValue: (Int64) -> Void
|
||||
|
||||
let sectionId: ItemListSectionId
|
||||
|
||||
let requestsNoInset: Bool = true
|
||||
|
||||
init(theme: PresentationTheme, title: String, currency: String, value: String, numericValue: Int64, availableVariants: [(String, Int64)], sectionId: ItemListSectionId, updateValue: @escaping (Int64) -> Void) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.currency = currency
|
||||
self.value = value
|
||||
self.numericValue = numericValue
|
||||
self.availableVariants = availableVariants
|
||||
self.updateValue = updateValue
|
||||
self.sectionId = sectionId
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = BotCheckoutTipItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? BotCheckoutTipItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selectable: Bool = false
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
private let finalFont = Font.semibold(17.0)
|
||||
|
||||
private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
var insets = UIEdgeInsets()
|
||||
switch neighbors.top {
|
||||
case .otherSection:
|
||||
insets.top += 8.0
|
||||
case .none, .sameSection:
|
||||
break
|
||||
}
|
||||
switch neighbors.bottom {
|
||||
case .none, .otherSection:
|
||||
insets.bottom += 8.0
|
||||
case .sameSection:
|
||||
break
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
private final class TipValueNode: ASDisplayNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private let button: HighlightTrackingButtonNode
|
||||
|
||||
private var currentBackgroundColor: UIColor?
|
||||
|
||||
var action: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
self.button = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.button)
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.action?()
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> CGFloat {
|
||||
var updateBackground = false
|
||||
let backgroundColor = isHighlighted ? UIColor(rgb: 0x00A650) : UIColor(rgb: 0xE5F6ED)
|
||||
if let currentBackgroundColor = self.currentBackgroundColor {
|
||||
if !currentBackgroundColor.isEqual(backgroundColor) {
|
||||
updateBackground = true
|
||||
}
|
||||
} else {
|
||||
updateBackground = true
|
||||
}
|
||||
if updateBackground {
|
||||
self.currentBackgroundColor = backgroundColor
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x00A650))
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height))
|
||||
|
||||
let minWidth: CGFloat = 80.0
|
||||
|
||||
let calculatedWidth = max(titleSize.width + 16.0 * 2.0, minWidth)
|
||||
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
let size = CGSize(width: calculatedWidth, height: height)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return size.width
|
||||
}
|
||||
}
|
||||
|
||||
class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate {
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
private let textNode: TextFieldNode
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private var valueNodes: [TipValueNode] = []
|
||||
|
||||
private var item: BotCheckoutTipItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = TextFieldNode()
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.textField.delegate = self
|
||||
self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: BotCheckoutTipItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
||||
return { item, params, neighbors in
|
||||
//let rightInset: CGFloat = 16.0 + params.rightInset
|
||||
|
||||
let labelsContentHeight: CGFloat = 34.0
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: labelsContentHeight)
|
||||
if !item.availableVariants.isEmpty {
|
||||
contentSize.height += 75.0
|
||||
}
|
||||
|
||||
let insets = priceItemInsets(neighbors)
|
||||
|
||||
let textFont: UIFont
|
||||
let textColor: UIColor
|
||||
|
||||
textFont = titleFont
|
||||
textColor = item.theme.list.itemSecondaryTextColor
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Enter Custom", font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = labelApply()
|
||||
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((labelsContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - labelLayout.size.width, y: floor((labelsContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
|
||||
let text: String
|
||||
if item.numericValue == 0 {
|
||||
text = ""
|
||||
} else {
|
||||
text = formatCurrencyAmount(item.numericValue, currency: item.currency)
|
||||
}
|
||||
if strongSelf.textNode.textField.text ?? "" != text {
|
||||
strongSelf.textNode.textField.text = text
|
||||
strongSelf.labelNode.isHidden = !text.isEmpty
|
||||
}
|
||||
|
||||
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: titleFont]
|
||||
strongSelf.textNode.textField.font = titleFont
|
||||
|
||||
strongSelf.textNode.textField.textColor = textColor
|
||||
strongSelf.textNode.textField.textAlignment = .right
|
||||
strongSelf.textNode.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance
|
||||
strongSelf.textNode.textField.keyboardType = .decimalPad
|
||||
strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight))
|
||||
|
||||
let valueHeight: CGFloat = 52.0
|
||||
let valueY: CGFloat = labelsContentHeight + 9.0
|
||||
|
||||
var index = 0
|
||||
var variantsOffset: CGFloat = 16.0
|
||||
for (variantText, variantValue) in item.availableVariants {
|
||||
if index != 0 {
|
||||
variantsOffset += 12.0
|
||||
}
|
||||
|
||||
let valueNode: TipValueNode
|
||||
if strongSelf.valueNodes.count > index {
|
||||
valueNode = strongSelf.valueNodes[index]
|
||||
} else {
|
||||
valueNode = TipValueNode()
|
||||
strongSelf.valueNodes.append(valueNode)
|
||||
strongSelf.scrollNode.addSubnode(valueNode)
|
||||
}
|
||||
let nodeWidth = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight)
|
||||
valueNode.action = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.item?.updateValue(variantValue)
|
||||
}
|
||||
valueNode.frame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight))
|
||||
variantsOffset += nodeWidth
|
||||
index += 1
|
||||
}
|
||||
|
||||
variantsOffset += 16.0
|
||||
|
||||
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY)))
|
||||
strongSelf.scrollNode.view.contentSize = CGSize(width: variantsOffset, height: strongSelf.scrollNode.frame.height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func textFieldTextChanged(_ textField: UITextField) {
|
||||
let text = textField.text ?? ""
|
||||
self.labelNode.isHidden = !text.isEmpty
|
||||
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
if text.isEmpty {
|
||||
item.updateValue(0)
|
||||
return
|
||||
}
|
||||
|
||||
var cleanText = ""
|
||||
for c in text {
|
||||
if c.isNumber {
|
||||
cleanText.append(c)
|
||||
} else if c == "," {
|
||||
cleanText.append(".")
|
||||
}
|
||||
}
|
||||
|
||||
guard let doubleValue = Double(cleanText) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) {
|
||||
item.updateValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard let item = self.item else {
|
||||
return false
|
||||
}
|
||||
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
@@ -155,18 +155,18 @@ enum BotReceiptEntry: ItemListNodeEntry {
|
||||
case let .header(theme, invoice, botName):
|
||||
return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section)
|
||||
case let .price(_, theme, text, value, isFinal):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section)
|
||||
case let .paymentMethod(theme, text, value):
|
||||
return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: false, sectionId: self.section)
|
||||
case let .paymentMethod(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .shippingInfo(theme, text, value):
|
||||
case let .shippingInfo(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .shippingMethod(theme, text, value):
|
||||
case let .shippingMethod(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .nameInfo(theme, text, value):
|
||||
case let .nameInfo(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .emailInfo(theme, text, value):
|
||||
case let .emailInfo(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
case let .phoneInfo(theme, text, value):
|
||||
case let .phoneInfo(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user