From 6da7ed5bfba5fef78f11665b8e85e535e02fd9ec Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 15:11:42 +0400 Subject: [PATCH 1/7] Payment updates --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + submodules/BotPaymentsUI/BUILD | 3 + .../Sources/BotCheckoutActionButton.swift | 30 ++- .../Sources/BotCheckoutController.swift | 48 ++-- .../Sources/BotCheckoutControllerNode.swift | 244 +++++++++++++++++- .../Sources/BotReceiptControllerNode.swift | 4 +- .../Payments/BotPaymentForm.swift | 73 +++--- 7 files changed, 333 insertions(+), 71 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index aa5535ff96..7be7c589b5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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$@**"; diff --git a/submodules/BotPaymentsUI/BUILD b/submodules/BotPaymentsUI/BUILD index 30ad6de487..45cfed992b 100644 --- a/submodules/BotPaymentsUI/BUILD +++ b/submodules/BotPaymentsUI/BUILD @@ -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", diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index b7aa1ea18d..1da8fd8434 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -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() @@ -85,12 +92,20 @@ final class BotCheckoutActionButton: HighlightableButtonNode { self.placeholderNode = nil 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 } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 2c8be1e5c4..39b3e3251e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -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 { @@ -39,28 +42,35 @@ public final class BotCheckoutController: ViewController { return .generic } |> mapToSignal { paymentForm -> Signal 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 in + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId)) + |> castError(FetchError.self) + |> mapToSignal { botPeer -> Signal 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 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 - )) } } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 9684509df6..8941aaccb8 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -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) diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index fb7524d351..137d2fac26 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 3f71144f59..06ff9acc56 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -44,12 +44,17 @@ public struct BotPaymentInvoice : Equatable { public var max: Int64 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) } } } From 7cb9f925bd51dd67e9b949ed4b432124cd989206 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 16:10:13 +0400 Subject: [PATCH 2/7] Transcription animation improvements --- submodules/Display/Source/ListView.swift | 18 +++++++++++++++--- .../Source/ListViewIntermediateState.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 6 +++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index c53a1effa0..25a669b16c 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -4239,9 +4239,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if itemNode.apparentFrame.maxY <= visualInsets.top { offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) - } else if invertOffsetDirection { - if itemNode.apparentFrame.minY - apparentHeightDelta < visualInsets.top { - let overflowOffset = visualInsets.top - (itemNode.apparentFrame.minY - apparentHeightDelta) + } else if invertOffsetDirection /*&& itemNode.frame.height < self.visibleSize.height*/ { + if self.scroller.contentOffset.y < 1.0 { + /*let overflowOffset = visualInsets.top - (itemNode.apparentFrame.minY - apparentHeightDelta) let remainingOffset = apparentHeightDelta - overflowOffset offsetRanges.offset(IndexRange(first: 0, last: index), offset: -remainingOffset) @@ -4255,6 +4255,18 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } + offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta)*/ + + var offsetDelta = apparentHeightDelta + if offsetDelta < 0.0 { + let maxDelta = visualInsets.top - itemNode.apparentFrame.maxY + if maxDelta > offsetDelta { + let remainingOffset = maxDelta - offsetDelta + offsetRanges.offset(IndexRange(first: 0, last: index), offset: remainingOffset) + offsetDelta = maxDelta + } + } + offsetRanges.offset(IndexRange(first: index + 1, last: Int.max), offset: offsetDelta) } else { offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index 0edec15664..fcc91cc9a6 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -812,7 +812,7 @@ struct ListViewState { for node in self.nodes { i += 1 if node.index == itemIndex { - if isAnimated { + if !isAnimated { let offsetDirection: ListViewInsertionOffsetDirection if let direction = direction { offsetDirection = ListViewInsertionOffsetDirection(direction) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index ac19923892..f2375eb673 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -42,7 +42,11 @@ private func transcribedText(message: Message) -> TranscribedText? { if !attribute.text.isEmpty { return .success(text: attribute.text, isPending: attribute.isPending) } else { - return .error + if attribute.isPending { + return nil + } else { + return .error + } } } } From 3f3741ab89c5c73e84945b2b176094e7b91c2aae Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 16:30:49 +0400 Subject: [PATCH 3/7] Fix webpage transcription animation --- .../ChatMessageAttachedContentNode.swift | 8 +++++++- .../ChatMessageInteractiveFileNode.swift | 17 +++++++++++------ .../ChatMessageWebpageBubbleContentNode.swift | 5 +++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 8941e6cb13..8f7d0bc68c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -235,6 +235,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)? var activateAction: (() -> Void)? + var requestUpdateLayout: (() -> Void)? var visibility: ListViewItemNodeVisibility = .none { didSet { @@ -837,7 +838,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.theme = presentationData.theme strongSelf.lineNode.image = lineImage - strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) + animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil) strongSelf.lineNode.isHidden = !displayLine strongSelf.textNode.displaysAsynchronously = !isPreview @@ -931,6 +932,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.openMedia?(.default) } } + contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in + if let strongSelf = strongSelf { + strongSelf.requestUpdateLayout?() + } + } } if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index f2375eb673..eb360e5b94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -349,16 +349,21 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var shouldBeginTranscription = false var shouldExpandNow = false - if let result = transcribedText(message: message) { + + if case .expanded = self.audioTranscriptionState { shouldExpandNow = true - - if case let .success(_, isPending) = result { - shouldBeginTranscription = isPending + } else { + if let result = transcribedText(message: message) { + shouldExpandNow = true + + if case let .success(_, isPending) = result { + shouldBeginTranscription = isPending + } else { + shouldBeginTranscription = true + } } else { shouldBeginTranscription = true } - } else { - shouldBeginTranscription = true } if shouldBeginTranscription { diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 2c4c0321f9..f7e60862b9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -96,6 +96,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } } + self.contentNode.requestUpdateLayout = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id) + } + } } required init?(coder aDecoder: NSCoder) { From 573215caf956d3f6fce0a2228d41c27a53a6d43e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 17:41:55 +0400 Subject: [PATCH 4/7] Fix flag --- .../Sources/State/AccountStateManagementUtils.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 61ffae2899..04c75665dd 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1102,8 +1102,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } case let .updateTranscribeAudio(flags, transcriptionId, text): - let isPending = (flags & (1 << 0)) != 0 - updatedState.updateAudioTranscription(id: transcriptionId, isPending: isPending, text: text) + let isFinal = (flags & (1 << 0)) != 0 + updatedState.updateAudioTranscription(id: transcriptionId, isPending: !isFinal, text: text) case let .updateNotifySettings(apiPeer, apiNotificationSettings): switch apiPeer { case let .notifyPeer(peer): From ed84852ba885eae2f65ab4b1f9247af6aa70c706 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 17:42:12 +0400 Subject: [PATCH 5/7] Remove experimental emoji --- submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index a35e0bedd8..06ddecf35f 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -667,7 +667,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - if let presentationContext = presentationContext { + /*if let presentationContext = presentationContext { self.emojiViewProvider = { [weak self, weak presentationContext] emoji in guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { return UIView() @@ -675,7 +675,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) } - } + }*/ } required init?(coder aDecoder: NSCoder) { From 965713eef2be952243443a956c9604d96fc9dd38 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 19:11:06 +0400 Subject: [PATCH 6/7] Add more logs [skip ci] --- .../Sources/Account/Account.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 7834869378..a59d03908b 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1344,37 +1344,55 @@ public func standaloneStateManager( useCaches: false, removeDatabaseOnError: false ) + + Logger.shared.log("StandaloneStateManager", "Prepare request postbox") return postbox |> take(1) |> mapToSignal { result -> Signal in switch result { case .upgrading: + Logger.shared.log("StandaloneStateManager", "Received postbox: upgrading") + return .single(nil) case .error: + Logger.shared.log("StandaloneStateManager", "Received postbox: error") + return .single(nil) case let .postbox(postbox): + Logger.shared.log("StandaloneStateManager", "Received postbox: valid") + return accountManager.transaction { transaction -> (LocalizationSettings?, ProxySettings?) in return (nil, transaction.getSharedData(SharedDataKeys.proxySettings)?.get(ProxySettings.self)) } |> mapToSignal { localizationSettings, proxySettings -> Signal in + Logger.shared.log("StandaloneStateManager", "Received settings") + return postbox.transaction { transaction -> (PostboxCoding?, LocalizationSettings?, ProxySettings?, NetworkSettings?) in let state = transaction.getState() return (state, localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self)) } |> mapToSignal { accountState, localizationSettings, proxySettings, networkSettings -> Signal in + Logger.shared.log("StandaloneStateManager", "Received state") + let keychain = makeExclusiveKeychain(id: id, postbox: postbox) if let accountState = accountState { switch accountState { case _ as UnauthorizedAccountState: + Logger.shared.log("StandaloneStateManager", "state is UnauthorizedAccountState") + return .single(nil) case let authorizedState as AuthorizedAccountState: + Logger.shared.log("StandaloneStateManager", "state is valid") + return postbox.transaction { transaction -> String? in return (transaction.getPeer(authorizedState.peerId) as? TelegramUser)?.phone } |> mapToSignal { phoneNumber in + Logger.shared.log("StandaloneStateManager", "received phone number") + return initializedNetwork( accountId: id, arguments: networkArguments, @@ -1389,6 +1407,8 @@ public func standaloneStateManager( phoneNumber: phoneNumber ) |> map { network -> AccountStateManager? in + Logger.shared.log("StandaloneStateManager", "received network") + return AccountStateManager( accountPeerId: authorizedState.peerId, accountManager: accountManager, @@ -1404,6 +1424,8 @@ public func standaloneStateManager( } } default: + Logger.shared.log("StandaloneStateManager", "Unexpected accountState") + assertionFailure("Unexpected accountState \(accountState)") return .single(nil) } From aae22dbff5a90f42b30b83e7e7e014c597be5774 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 31 May 2022 22:02:53 +0400 Subject: [PATCH 7/7] Disable environment reference deduplication [skip ci] --- submodules/ComponentFlow/Source/Base/Environment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/ComponentFlow/Source/Base/Environment.swift b/submodules/ComponentFlow/Source/Base/Environment.swift index c0c8e244cc..111f2235c8 100644 --- a/submodules/ComponentFlow/Source/Base/Environment.swift +++ b/submodules/ComponentFlow/Source/Base/Environment.swift @@ -171,7 +171,7 @@ public struct EnvironmentBuilder { } public static func buildExpression(_ expression: EnvironmentValue) -> Partial { - return Partial(value: expression) + return Partial(value: EnvironmentValue(expression.value)) } public static func buildBlock(_ t1: Partial) -> Environment {