mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Payment updates
This commit is contained in:
parent
1b0b7660db
commit
6da7ed5bfb
@ -7665,3 +7665,5 @@ Sorry for the inconvenience.";
|
||||
"Premium.Limits.FoldersInfo" = "Organize your chats into 20 folders";
|
||||
"Premium.Limits.ChatsPerFolderInfo" = "Add up to 200 chats into each of your folders";
|
||||
"Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers";
|
||||
|
||||
"Bot.AccepRecurrentInfo" = "I accept [Terms of Service]() of **%1$@**";
|
||||
|
@ -25,6 +25,9 @@ swift_library(
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/CheckNode:CheckNode",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/Markdown:Markdown",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -6,8 +6,8 @@ import PassKit
|
||||
import ShimmerEffect
|
||||
|
||||
enum BotCheckoutActionButtonState: Equatable {
|
||||
case active(String)
|
||||
case applePay
|
||||
case active(text: String, isEnabled: Bool)
|
||||
case applePay(isEnabled: Bool)
|
||||
case placeholder
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
static var height: CGFloat = 52.0
|
||||
|
||||
private var activeFillColor: UIColor
|
||||
private var inactiveFillColor: UIColor
|
||||
private var foregroundColor: UIColor
|
||||
|
||||
private let activeBackgroundNode: ASImageNode
|
||||
@ -28,17 +29,23 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
|
||||
init(activeFillColor: UIColor, foregroundColor: UIColor) {
|
||||
private var activeImage: UIImage?
|
||||
private var inactiveImage: UIImage?
|
||||
|
||||
init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) {
|
||||
self.activeFillColor = activeFillColor
|
||||
self.inactiveFillColor = inactiveFillColor
|
||||
self.foregroundColor = foregroundColor
|
||||
|
||||
let diameter: CGFloat = 20.0
|
||||
self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
|
||||
self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor)
|
||||
|
||||
self.activeBackgroundNode = ASImageNode()
|
||||
self.activeBackgroundNode.displaysAsynchronously = false
|
||||
self.activeBackgroundNode.displayWithoutProcessing = true
|
||||
self.activeBackgroundNode.isLayerBacked = true
|
||||
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
|
||||
self.activeBackgroundNode.image = self.activeImage
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
@ -75,7 +82,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
var labelSize = self.labelNode.bounds.size
|
||||
if let state = self.state {
|
||||
switch state {
|
||||
case let .active(title):
|
||||
case let .active(title, isEnabled):
|
||||
if let applePayButton = self.applePayButton {
|
||||
self.applePayButton = nil
|
||||
applePayButton.removeFromSuperview()
|
||||
@ -86,11 +93,19 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
placeholderNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let image = isEnabled ? self.activeImage : self.inactiveImage
|
||||
if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image {
|
||||
self.activeBackgroundNode.image = image
|
||||
self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
|
||||
} else {
|
||||
self.activeBackgroundNode.image = image
|
||||
}
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = labelApply()
|
||||
labelSize = labelLayout.size
|
||||
case .applePay:
|
||||
case let .applePay(isEnabled):
|
||||
if self.applePayButton == nil {
|
||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
let applePayButton: PKPaymentButton
|
||||
@ -102,6 +117,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
|
||||
self.view.addSubview(applePayButton)
|
||||
self.applePayButton = applePayButton
|
||||
applePayButton.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,13 +15,16 @@ public final class BotCheckoutController: ViewController {
|
||||
|
||||
let form: BotPaymentForm
|
||||
let validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
let botPeer: EnginePeer?
|
||||
|
||||
private init(
|
||||
form: BotPaymentForm,
|
||||
validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
validatedFormInfo: BotPaymentValidatedFormInfo?,
|
||||
botPeer: EnginePeer?
|
||||
) {
|
||||
self.form = form
|
||||
self.validatedFormInfo = validatedFormInfo
|
||||
self.botPeer = botPeer
|
||||
}
|
||||
|
||||
public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
|
||||
@ -39,28 +42,35 @@ public final class BotCheckoutController: ViewController {
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
|
||||
if let current = paymentForm.savedInfo {
|
||||
return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current)
|
||||
|> mapError { _ -> FetchError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> InputData in
|
||||
return InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: result
|
||||
)
|
||||
}
|
||||
|> `catch` { _ -> Signal<InputData, FetchError> in
|
||||
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId))
|
||||
|> castError(FetchError.self)
|
||||
|> mapToSignal { botPeer -> Signal<InputData, FetchError> in
|
||||
if let current = paymentForm.savedInfo {
|
||||
return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current)
|
||||
|> mapError { _ -> FetchError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> InputData in
|
||||
return InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: result,
|
||||
botPeer: botPeer
|
||||
)
|
||||
}
|
||||
|> `catch` { _ -> Signal<InputData, FetchError> in
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil,
|
||||
botPeer: botPeer
|
||||
))
|
||||
}
|
||||
} else {
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil
|
||||
validatedFormInfo: nil,
|
||||
botPeer: botPeer
|
||||
))
|
||||
}
|
||||
} else {
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ import PasswordSetupUI
|
||||
import Stripe
|
||||
import LocalAuth
|
||||
import OverlayStatusController
|
||||
import CheckNode
|
||||
import TextFormat
|
||||
import Markdown
|
||||
|
||||
final class BotCheckoutControllerArguments {
|
||||
fileprivate let account: Account
|
||||
@ -489,6 +492,160 @@ private func availablePaymentMethods(form: BotPaymentForm, current: BotCheckoutP
|
||||
return methods
|
||||
}
|
||||
|
||||
private final class RecurrentConfirmationNode: ASDisplayNode {
|
||||
private let isAcceptedUpdated: (Bool) -> Void
|
||||
private let openTerms: () -> Void
|
||||
|
||||
private var checkNode: InteractiveCheckNode?
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
init(isAcceptedUpdated: @escaping (Bool) -> Void, openTerms: @escaping () -> Void) {
|
||||
self.isAcceptedUpdated = isAcceptedUpdated
|
||||
self.openTerms = openTerms
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
super.init()
|
||||
|
||||
self.textNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.textNode.tapAttributeAction = { [weak self] attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
self?.openTerms()
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let checkNode = self.checkNode else {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
checkNode.setSelected(!checkNode.selected, animated: true)
|
||||
checkNode.valueChanged?(checkNode.selected)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let (_, attributes) = self.textNode.attributesAtPoint(self.view.convert(point, to: self.textNode.view)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, botName: String, width: CGFloat, sideInset: CGFloat) -> CGFloat {
|
||||
let spacing: CGFloat = 16.0
|
||||
let topInset: CGFloat = 8.0
|
||||
|
||||
let checkNode: InteractiveCheckNode
|
||||
if let current = self.checkNode {
|
||||
checkNode = current
|
||||
} else {
|
||||
checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: presentationData.theme.list.itemCheckColors.fillColor, strokeColor: presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
checkNode.valueChanged = { [weak self] value in
|
||||
self?.isAcceptedUpdated(value)
|
||||
}
|
||||
self.checkNode = checkNode
|
||||
self.addSubnode(checkNode)
|
||||
}
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
|
||||
self.textNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withAlphaComponent(0.3)
|
||||
|
||||
let attributedText = parseMarkdownIntoAttributedString(
|
||||
presentationData.strings.Bot_AccepRecurrentInfo(botName).string,
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.itemAccentColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.textNode.attributedText = attributedText
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - spacing - checkSize.width, height: .greatestFiniteMagnitude))
|
||||
|
||||
let height = textSize.height + 15.0
|
||||
|
||||
let contentWidth = checkSize.width + spacing + textSize.width
|
||||
let contentOriginX = sideInset + floor((width - sideInset * 2.0 - contentWidth) / 2.0)
|
||||
|
||||
checkNode.frame = CGRect(origin: CGPoint(x: contentOriginX, y: topInset + floor((height - checkSize.height) / 2.0)), size: checkSize)
|
||||
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: contentOriginX + checkSize.width + spacing, y: topInset + floor((height - textSize.height) / 2.0)), size: textSize)
|
||||
|
||||
return height
|
||||
}
|
||||
}
|
||||
|
||||
private final class ActionButtonPanelNode: ASDisplayNode {
|
||||
private(set) var isAccepted: Bool = false
|
||||
var isAcceptedUpdated: (() -> Void)?
|
||||
var openRecurrentTerms: (() -> Void)?
|
||||
private var recurrentConfirmationNode: RecurrentConfirmationNode?
|
||||
|
||||
func update(presentationData: PresentationData, layout: ContainerViewLayout, invoice: BotPaymentInvoice?, botName: String?) -> (CGFloat, CGFloat) {
|
||||
let bottomPanelVerticalInset: CGFloat = 16.0
|
||||
|
||||
var height = max(layout.intrinsicInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
|
||||
var actionButtonOffset: CGFloat = bottomPanelVerticalInset
|
||||
|
||||
if let invoice = invoice, let recurrentInfo = invoice.recurrentInfo, let botName = botName {
|
||||
let recurrentConfirmationNode: RecurrentConfirmationNode
|
||||
if let current = self.recurrentConfirmationNode {
|
||||
recurrentConfirmationNode = current
|
||||
} else {
|
||||
recurrentConfirmationNode = RecurrentConfirmationNode(isAcceptedUpdated: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isAccepted = value
|
||||
strongSelf.isAcceptedUpdated?()
|
||||
}, openTerms: { [weak self] in
|
||||
self?.openRecurrentTerms?()
|
||||
})
|
||||
self.recurrentConfirmationNode = recurrentConfirmationNode
|
||||
self.addSubnode(recurrentConfirmationNode)
|
||||
}
|
||||
|
||||
let _ = recurrentInfo
|
||||
|
||||
let recurrentConfirmationHeight = recurrentConfirmationNode.update(presentationData: presentationData, botName: botName, width: layout.size.width, sideInset: layout.safeInsets.left + 33.0)
|
||||
recurrentConfirmationNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: recurrentConfirmationHeight))
|
||||
|
||||
actionButtonOffset += recurrentConfirmationHeight
|
||||
} else if let recurrentConfirmationNode = self.recurrentConfirmationNode {
|
||||
self.recurrentConfirmationNode = nil
|
||||
|
||||
recurrentConfirmationNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
height += actionButtonOffset - bottomPanelVerticalInset
|
||||
|
||||
return (height, actionButtonOffset)
|
||||
}
|
||||
}
|
||||
|
||||
final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate {
|
||||
private weak var controller: BotCheckoutController?
|
||||
private let navigationBar: NavigationBar
|
||||
@ -509,6 +666,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
|
||||
private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?, Int64?)?>(nil)
|
||||
private var paymentFormValue: BotPaymentForm?
|
||||
private var botPeerValue: EnginePeer?
|
||||
private var currentFormInfo: BotPaymentRequestedInfo?
|
||||
private var currentValidatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
private var currentShippingOptionId: String?
|
||||
@ -516,7 +674,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
private var currentTipAmount: Int64?
|
||||
private var formRequestDisposable: Disposable?
|
||||
|
||||
private let actionButtonPanelNode: ASDisplayNode
|
||||
private let actionButtonPanelNode: ActionButtonPanelNode
|
||||
private let actionButtonPanelSeparator: ASDisplayNode
|
||||
private let actionButton: BotCheckoutActionButton
|
||||
private let inProgressDimNode: ASDisplayNode
|
||||
@ -585,13 +743,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
return (ItemListPresentationData(presentationData), (nodeState, arguments))
|
||||
}
|
||||
|
||||
self.actionButtonPanelNode = ASDisplayNode()
|
||||
self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
self.actionButtonPanelNode = ActionButtonPanelNode()
|
||||
|
||||
self.actionButtonPanelSeparator = ASDisplayNode()
|
||||
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, inactiveFillColor: self.presentationData.theme.list.itemDisabledTextColor.mixedWith(self.presentationData.theme.list.blocksBackgroundColor, alpha: 0.7), foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
self.actionButton.setState(.placeholder)
|
||||
|
||||
self.inProgressDimNode = ASDisplayNode()
|
||||
@ -603,6 +759,22 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
|
||||
self.arguments = arguments
|
||||
|
||||
self.actionButtonPanelNode.isAcceptedUpdated = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateActionButton()
|
||||
}
|
||||
|
||||
self.actionButtonPanelNode.openRecurrentTerms = { [weak self] in
|
||||
guard let strongSelf = self, let paymentForm = strongSelf.paymentFormValue, let recurrentInfo = paymentForm.invoice.recurrentInfo else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: recurrentInfo.termsUrl, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {
|
||||
self?.view.endEditing(true)
|
||||
})
|
||||
}
|
||||
|
||||
openInfoImpl = { [weak self] focus in
|
||||
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
|
||||
strongSelf.controller?.view.endEditing(true)
|
||||
@ -924,6 +1096,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil)
|
||||
}
|
||||
strongSelf.paymentFormValue = formAndValidatedInfo.form
|
||||
strongSelf.botPeerValue = formAndValidatedInfo.botPeer
|
||||
strongSelf.currentFormInfo = savedInfo
|
||||
strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo
|
||||
if let savedCredentials = formAndValidatedInfo.form.savedCredentials {
|
||||
@ -959,6 +1132,38 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
self.actionButtonPanelNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
self.visibleBottomContentOffsetChanged = { [weak self] offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let panelColor: UIColor
|
||||
let separatorColor: UIColor
|
||||
switch offset {
|
||||
case let .known(value):
|
||||
if value > 10.0 {
|
||||
panelColor = strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
separatorColor = strongSelf.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
} else {
|
||||
panelColor = .clear
|
||||
separatorColor = .clear
|
||||
}
|
||||
default:
|
||||
panelColor = strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
separatorColor = strongSelf.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
|
||||
if strongSelf.actionButtonPanelNode.backgroundColor != panelColor {
|
||||
transition.updateBackgroundColor(node: strongSelf.actionButtonPanelNode, color: panelColor)
|
||||
}
|
||||
if strongSelf.actionButtonPanelSeparator.backgroundColor != separatorColor {
|
||||
transition.updateBackgroundColor(node: strongSelf.actionButtonPanelSeparator, color: separatorColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -971,22 +1176,34 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
private func updateActionButton() {
|
||||
let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount)
|
||||
let payString: String
|
||||
var isButtonEnabled = true
|
||||
if let paymentForm = self.paymentFormValue, totalAmount > 0 {
|
||||
payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).string
|
||||
|
||||
if let _ = paymentForm.invoice.recurrentInfo {
|
||||
if !self.actionButtonPanelNode.isAccepted {
|
||||
isButtonEnabled = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payString = self.presentationData.strings.CheckoutInfo_Pay
|
||||
}
|
||||
|
||||
self.actionButton.isEnabled = isButtonEnabled
|
||||
|
||||
if let currentPaymentMethod = self.currentPaymentMethod {
|
||||
switch currentPaymentMethod {
|
||||
case .applePay:
|
||||
self.actionButton.setState(.applePay)
|
||||
self.actionButton.setState(.applePay(isEnabled: isButtonEnabled))
|
||||
default:
|
||||
self.actionButton.setState(.active(payString))
|
||||
self.actionButton.setState(.active(text: payString, isEnabled: isButtonEnabled))
|
||||
}
|
||||
} else {
|
||||
self.actionButton.setState(.active(payString))
|
||||
self.actionButton.setState(.active(text: payString, isEnabled: isButtonEnabled))
|
||||
}
|
||||
self.actionButtonPanelNode.isHidden = false
|
||||
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateIsInProgress(_ value: Bool) {
|
||||
@ -1006,13 +1223,18 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
var updatedInsets = layout.intrinsicInsets
|
||||
|
||||
let bottomPanelHorizontalInset: CGFloat = 16.0
|
||||
let bottomPanelVerticalInset: CGFloat = 16.0
|
||||
let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
|
||||
|
||||
var botName: String?
|
||||
if let botPeer = self.botPeerValue {
|
||||
botName = botPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
||||
}
|
||||
|
||||
let (bottomPanelHeight, actionButtonOffset) = self.actionButtonPanelNode.update(presentationData: self.presentationData, layout: layout, invoice: self.paymentFormValue?.invoice, botName: botName)
|
||||
|
||||
transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight)))
|
||||
transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: actionButtonOffset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height))
|
||||
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
||||
self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition)
|
||||
|
||||
|
@ -305,8 +305,8 @@ final class BotReceiptControllerNode: ItemListControllerNode {
|
||||
self.actionButtonPanelSeparator = ASDisplayNode()
|
||||
self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
|
||||
self.actionButton.setState(.active(self.presentationData.strings.Common_Done))
|
||||
self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, inactiveFillColor: self.presentationData.theme.list.itemDisabledTextColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor)
|
||||
self.actionButton.setState(.active(text: self.presentationData.strings.Common_Done, isEnabled: true))
|
||||
|
||||
super.init(controller: controller, navigationBar: navigationBar, state: signal)
|
||||
|
||||
|
@ -45,11 +45,16 @@ public struct BotPaymentInvoice : Equatable {
|
||||
public var suggested: [Int64]
|
||||
}
|
||||
|
||||
public struct RecurrentInfo: Equatable {
|
||||
public var termsUrl: String
|
||||
}
|
||||
|
||||
public let isTest: Bool
|
||||
public let requestedFields: BotPaymentInvoiceFields
|
||||
public let currency: String
|
||||
public let prices: [BotPaymentPrice]
|
||||
public let tip: Tip?
|
||||
public let recurrentInfo: RecurrentInfo?
|
||||
}
|
||||
|
||||
public struct BotPaymentNativeProvider : Equatable {
|
||||
@ -124,39 +129,43 @@ public enum BotPaymentFormRequestError {
|
||||
extension BotPaymentInvoice {
|
||||
init(apiInvoice: Api.Invoice) {
|
||||
switch apiInvoice {
|
||||
case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, _):
|
||||
var fields = BotPaymentInvoiceFields()
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
fields.insert(.name)
|
||||
case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, recurrentTermsUrl):
|
||||
var fields = BotPaymentInvoiceFields()
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
fields.insert(.name)
|
||||
}
|
||||
if (flags & (1 << 2)) != 0 {
|
||||
fields.insert(.phone)
|
||||
}
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
fields.insert(.email)
|
||||
}
|
||||
if (flags & (1 << 4)) != 0 {
|
||||
fields.insert(.shippingAddress)
|
||||
}
|
||||
if (flags & (1 << 5)) != 0 {
|
||||
fields.insert(.flexibleShipping)
|
||||
}
|
||||
if (flags & (1 << 6)) != 0 {
|
||||
fields.insert(.phoneAvailableToProvider)
|
||||
}
|
||||
if (flags & (1 << 7)) != 0 {
|
||||
fields.insert(.emailAvailableToProvider)
|
||||
}
|
||||
var recurrentInfo: BotPaymentInvoice.RecurrentInfo?
|
||||
if let recurrentTermsUrl = recurrentTermsUrl {
|
||||
recurrentInfo = BotPaymentInvoice.RecurrentInfo(termsUrl: recurrentTermsUrl)
|
||||
}
|
||||
var parsedTip: BotPaymentInvoice.Tip?
|
||||
if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts {
|
||||
parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts)
|
||||
}
|
||||
self.init(isTest: (flags & (1 << 0)) != 0, requestedFields: fields, currency: currency, prices: prices.map {
|
||||
switch $0 {
|
||||
case let .labeledPrice(label, amount):
|
||||
return BotPaymentPrice(label: label, amount: amount)
|
||||
}
|
||||
if (flags & (1 << 2)) != 0 {
|
||||
fields.insert(.phone)
|
||||
}
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
fields.insert(.email)
|
||||
}
|
||||
if (flags & (1 << 4)) != 0 {
|
||||
fields.insert(.shippingAddress)
|
||||
}
|
||||
if (flags & (1 << 5)) != 0 {
|
||||
fields.insert(.flexibleShipping)
|
||||
}
|
||||
if (flags & (1 << 6)) != 0 {
|
||||
fields.insert(.phoneAvailableToProvider)
|
||||
}
|
||||
if (flags & (1 << 7)) != 0 {
|
||||
fields.insert(.emailAvailableToProvider)
|
||||
}
|
||||
var parsedTip: BotPaymentInvoice.Tip?
|
||||
if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts {
|
||||
parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts)
|
||||
}
|
||||
self.init(isTest: (flags & (1 << 0)) != 0, requestedFields: fields, currency: currency, prices: prices.map {
|
||||
switch $0 {
|
||||
case let .labeledPrice(label, amount):
|
||||
return BotPaymentPrice(label: label, amount: amount)
|
||||
}
|
||||
}, tip: parsedTip)
|
||||
}, tip: parsedTip, recurrentInfo: recurrentInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user