From 902263cf5950507ac57c2505a58b5e9b79113b7d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 6 May 2022 21:14:28 +0400 Subject: [PATCH] Various experiments --- .../Sources/AccountContext.swift | 4 +- .../Sources/BotCheckoutController.swift | 14 +- .../Sources/BotCheckoutControllerNode.swift | 12 +- .../Sources/BotCheckoutInfoController.swift | 8 +- .../BotCheckoutInfoControllerNode.swift | 8 +- .../Source/Base/CombinedComponent.swift | 15 +- .../ComponentFlow/Source/Base/Component.swift | 6 +- .../Source/Components/Button.swift | 4 +- .../Source/Host/ComponentHostView.swift | 7 +- .../Forms/CreditCardInputComponent/BUILD | 20 + .../Sources/CreditCardInputComponent.swift | 172 ++ .../Forms/PrefixSectionGroupComponent/BUILD | 19 + .../Sources/PrefixSectionGroupComponent.swift | 194 +++ .../Components/Forms/TextInputComponent/BUILD | 19 + .../Sources/TextInputComponent.swift | 86 + .../Components/MultilineTextComponent/BUILD | 4 +- .../Sources/MultilineTextComponent.swift | 36 +- .../Sources/ViewControllerComponent.swift | 26 +- .../Sources/InviteLinkHeaderItem.swift | 20 +- .../Sources/Items/ItemListCheckboxItem.swift | 25 +- submodules/Markdown/Source/Markdown.swift | 27 +- submodules/PaymentMethodUI/BUILD | 43 + .../AddPaymentMethodScheetScreen.swift | 416 +++++ .../Sources/PaymentCardEntryScreen.swift | 449 +++++ .../Sources/PaymentMethodListScreen.swift | 286 +++ .../CreateExternalMediaStreamScreen.swift | 12 +- .../PremiumUI/Sources/LimitScreen.swift | 4 +- submodules/SettingsUI/BUILD | 1 + .../Stripe}/STPFormTextField.h | 0 .../STPPaymentCardTextFieldViewModel.h | 0 .../Stripe/PublicHeaders/Stripe/Stripe.h | 1 + submodules/TelegramApi/Sources/Api0.swift | 9 +- submodules/TelegramApi/Sources/Api25.swift | 110 +- submodules/TelegramApi/Sources/Api27.swift | 42 +- submodules/TelegramApi/Sources/Api7.swift | 62 + .../MediaStreamVideoComponent.swift | 2 +- .../Payments/BotPaymentForm.swift | 131 +- .../Payments/TelegramEnginePayments.swift | 22 +- .../TelegramUI/Resources/PATTERN_static.svg | 1549 +++++++++++++++++ .../Resources/ptrnCAT_1162_1918.tgs | Bin 0 -> 16651 bytes .../Resources/ptrnDOG_0440_2284.tgs | Bin 0 -> 8365 bytes .../Resources/ptrnGLOB_0438_1553.tgs | Bin 0 -> 10308 bytes .../Resources/ptrnSLON_0906_1033.tgs | Bin 0 -> 6740 bytes .../TelegramUI/Sources/ChatController.swift | 4 +- .../ChatRecentActionsControllerNode.swift | 25 + .../TelegramUI/Sources/OpenResolvedUrl.swift | 24 + submodules/TelegramUI/Sources/OpenUrl.swift | 16 + .../Sources/PeerInfo/PeerInfoScreen.swift | 26 + .../TranslateUI/Sources/TranslateScreen.swift | 8 +- .../UrlHandling/Sources/UrlHandling.swift | 15 + submodules/WallpaperBackgroundNode/BUILD | 3 + .../Sources/WallpaperBackgroundNode.swift | 75 +- 52 files changed, 3896 insertions(+), 165 deletions(-) create mode 100644 submodules/Components/Forms/CreditCardInputComponent/BUILD create mode 100644 submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift create mode 100644 submodules/Components/Forms/PrefixSectionGroupComponent/BUILD create mode 100644 submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift create mode 100644 submodules/Components/Forms/TextInputComponent/BUILD create mode 100644 submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift create mode 100644 submodules/PaymentMethodUI/BUILD create mode 100644 submodules/PaymentMethodUI/Sources/AddPaymentMethodScheetScreen.swift create mode 100644 submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift create mode 100644 submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift rename submodules/Stripe/{Sources => PublicHeaders/Stripe}/STPFormTextField.h (100%) rename submodules/Stripe/{Sources => PublicHeaders/Stripe}/STPPaymentCardTextFieldViewModel.h (100%) create mode 100644 submodules/TelegramUI/Resources/PATTERN_static.svg create mode 100644 submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs create mode 100644 submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs create mode 100644 submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs create mode 100644 submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0f7cfdf475..eac8787137 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -252,13 +252,11 @@ public enum ResolvedUrl { case share(url: String?, text: String?, to: String?) case wallpaper(WallpaperUrlParameter) case theme(String) - #if ENABLE_WALLET - case wallet(address: String, amount: Int64?, comment: String?) - #endif case settings(ResolvedUrlSettingsSection) case joinVoiceChat(PeerId, String?) case importStickers case startAttach(peerId: PeerId, payload: String?) + case invoice(slug: String, invoice: TelegramMediaInvoice) } public enum NavigateToChatKeepStack { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 0f9fbea0ed..1ed4f74477 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -24,7 +24,7 @@ public final class BotCheckoutController: ViewController { self.validatedFormInfo = validatedFormInfo } - public static func fetch(context: AccountContext, messageId: EngineMessage.Id) -> Signal { + public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let themeParams: [String: Any] = [ "bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb), @@ -34,13 +34,13 @@ public final class BotCheckoutController: ViewController { "button_text_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.foregroundColor.argb) ] - return context.engine.payments.fetchBotPaymentForm(messageId: messageId, themeParams: themeParams) + return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams) |> mapError { _ -> FetchError in return .generic } |> mapToSignal { paymentForm -> Signal in if let current = paymentForm.savedInfo { - return context.engine.payments.validateBotPaymentForm(saveInfo: true, messageId: messageId, formInfo: current) + return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current) |> mapError { _ -> FetchError in return .generic } @@ -77,7 +77,7 @@ public final class BotCheckoutController: ViewController { private let context: AccountContext private let invoice: TelegramMediaInvoice - private let messageId: EngineMessage.Id + private let source: BotPaymentInvoiceSource private let completed: (String, EngineMessage.Id?) -> Void private var presentationData: PresentationData @@ -86,10 +86,10 @@ public final class BotCheckoutController: ViewController { private let inputData: Promise - public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: EngineMessage.Id, inputData: Promise, completed: @escaping (String, EngineMessage.Id?) -> Void) { + public init(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Promise, completed: @escaping (String, EngineMessage.Id?) -> Void) { self.context = context self.invoice = invoice - self.messageId = messageId + self.source = source self.inputData = inputData self.completed = completed @@ -113,7 +113,7 @@ public final class BotCheckoutController: ViewController { } override public func loadDisplayNode() { - let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, messageId: self.messageId, inputData: self.inputData, present: { [weak self] c, a in + let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, source: self.source, inputData: self.inputData, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismissAnimated: { [weak self] in self?.dismiss() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index ed4f5ec7dd..cd06521ab7 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -493,7 +493,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz private weak var controller: BotCheckoutController? private let navigationBar: NavigationBar private let context: AccountContext - private let messageId: EngineMessage.Id + private let source: BotPaymentInvoiceSource private let present: (ViewController, Any?) -> Void private let dismissAnimated: () -> Void private let completed: (String, EngineMessage.Id?) -> Void @@ -527,11 +527,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz private var passwordTip: String? private var passwordTipDisposable: Disposable? - init(controller: BotCheckoutController?, navigationBar: NavigationBar, context: AccountContext, invoice: TelegramMediaInvoice, messageId: EngineMessage.Id, inputData: Promise, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void, completed: @escaping (String, EngineMessage.Id?) -> Void) { + init(controller: BotCheckoutController?, navigationBar: NavigationBar, context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Promise, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void, completed: @escaping (String, EngineMessage.Id?) -> Void) { self.controller = controller self.navigationBar = navigationBar self.context = context - self.messageId = messageId + self.source = source self.present = present self.dismissAnimated = dismissAnimated self.completed = completed @@ -603,7 +603,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openInfoImpl = { [weak self] focus in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.controller?.view.endEditing(true) - strongSelf.present(BotCheckoutInfoController(context: context, invoice: paymentFormValue.invoice, messageId: messageId, initialFormInfo: currentFormInfo, focus: focus, formInfoUpdated: { formInfo, validatedInfo in + strongSelf.present(BotCheckoutInfoController(context: context, invoice: paymentFormValue.invoice, source: source, initialFormInfo: currentFormInfo, focus: focus, formInfoUpdated: { formInfo, validatedInfo in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue { strongSelf.currentFormInfo = formInfo strongSelf.currentValidatedFormInfo = validatedInfo @@ -1125,7 +1125,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz countryCode = paramsCountryCode } - let botPeerId = self.messageId.peerId + let botPeerId = paymentForm.paymentBotId let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: botPeerId) ) @@ -1239,7 +1239,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz let totalAmount = currentTotalPrice(paymentForm: paymentForm, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) let currencyValue = formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency) - self.payDisposable.set((self.context.engine.payments.sendBotPaymentForm(messageId: self.messageId, formId: paymentForm.id, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, tipAmount: tipAmount, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in + self.payDisposable.set((self.context.engine.payments.sendBotPaymentForm(source: self.source, formId: paymentForm.id, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, tipAmount: tipAmount, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.alpha = 0.0 diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift index a7a8d53eed..bee72bda48 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift @@ -30,7 +30,7 @@ final class BotCheckoutInfoController: ViewController { private let context: AccountContext private let invoice: BotPaymentInvoice - private let messageId: EngineMessage.Id + private let source: BotPaymentInvoiceSource private let initialFormInfo: BotPaymentRequestedInfo private let focus: BotCheckoutInfoControllerFocus @@ -46,14 +46,14 @@ final class BotCheckoutInfoController: ViewController { public init( context: AccountContext, invoice: BotPaymentInvoice, - messageId: EngineMessage.Id, + source: BotPaymentInvoiceSource, initialFormInfo: BotPaymentRequestedInfo, focus: BotCheckoutInfoControllerFocus, formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void ) { self.context = context self.invoice = invoice - self.messageId = messageId + self.source = source self.initialFormInfo = initialFormInfo self.focus = focus self.formInfoUpdated = formInfoUpdated @@ -80,7 +80,7 @@ final class BotCheckoutInfoController: ViewController { } override public func loadDisplayNode() { - self.displayNode = BotCheckoutInfoControllerNode(context: self.context, navigationBar: self.navigationBar, invoice: self.invoice, messageId: self.messageId, formInfo: self.initialFormInfo, focus: self.focus, theme: self.presentationData.theme, strings: self.presentationData.strings, dismiss: { [weak self] in + self.displayNode = BotCheckoutInfoControllerNode(context: self.context, navigationBar: self.navigationBar, invoice: self.invoice, source: self.source, formInfo: self.initialFormInfo, focus: self.focus, theme: self.presentationData.theme, strings: self.presentationData.strings, dismiss: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift index b7f66d3740..2ae9a23786 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift @@ -96,7 +96,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi private let context: AccountContext private weak var navigationBar: NavigationBar? private let invoice: BotPaymentInvoice - private let messageId: EngineMessage.Id + private let source: BotPaymentInvoiceSource private var focus: BotCheckoutInfoControllerFocus? private let dismiss: () -> Void @@ -130,7 +130,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi context: AccountContext, navigationBar: NavigationBar?, invoice: BotPaymentInvoice, - messageId: EngineMessage.Id, + source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo, focus: BotCheckoutInfoControllerFocus, theme: PresentationTheme, @@ -144,7 +144,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi self.context = context self.navigationBar = navigationBar self.invoice = invoice - self.messageId = messageId + self.source = source self.formInfo = formInfo self.focus = focus self.dismiss = dismiss @@ -367,7 +367,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi func verify() { self.isVerifying = true let formInfo = self.collectFormInfo() - self.verifyDisposable.set((self.context.engine.payments.validateBotPaymentForm(saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in + self.verifyDisposable.set((self.context.engine.payments.validateBotPaymentForm(saveInfo: self.saveInfoItem.isOn, source: self.source, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.formInfoUpdated(formInfo, result) } diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index 04453d4e04..cc0eea2a92 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -104,8 +104,9 @@ public final class _ConcreteChildComponent: _AnyChildC let context = view.context(component: component) EnvironmentBuilder._environment = context.erasedEnvironment - let _ = environment() + let environmentResult = environment() EnvironmentBuilder._environment = nil + context.erasedEnvironment = environmentResult return updateChildAnyComponent( id: self.id, @@ -288,9 +289,11 @@ public final class _EnvironmentChildComponent: _AnyChildCompone transition = .immediate } - EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment - let _ = environment() + let viewContext = view.context(component: component) + EnvironmentBuilder._environment = viewContext.erasedEnvironment + let environmentResult = environment() EnvironmentBuilder._environment = nil + viewContext.erasedEnvironment = environmentResult return updateChildAnyComponent( id: self.id, @@ -342,9 +345,11 @@ public final class _EnvironmentChildComponentFromMap: _AnyChild transition = .immediate } - EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment - let _ = environment() + let viewContext = view.context(component: component) + EnvironmentBuilder._environment = viewContext.erasedEnvironment + let environmentResult = environment() EnvironmentBuilder._environment = nil + viewContext.erasedEnvironment = environmentResult return updateChildAnyComponent( id: self.id, diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index bf533e3393..ea9a8c2bf2 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -26,7 +26,11 @@ class AnyComponentContext: _TypeErasedComponentContext { preconditionFailure() } var erasedEnvironment: _Environment { - return self.environment + get { + return self.environment + } set(value) { + self.environment = value as! Environment + } } let layoutResult: ComponentLayoutResult diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 8c635b8041..078558a048 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -23,13 +23,13 @@ public final class Button: Component { private init( content: AnyComponent, - minSize: CGSize?, + minSize: CGSize? = nil, tag: AnyObject? = nil, automaticHighlight: Bool = true, action: @escaping () -> Void ) { self.content = content - self.minSize = nil + self.minSize = minSize self.tag = tag self.automaticHighlight = automaticHighlight self.action = action diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index 6e3faf3711..455548fab8 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -17,7 +17,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? { return nil } -public final class ComponentHostView: UIView { +public final class ComponentHostView: UIView { private var currentComponent: AnyComponent? private var currentContainerSize: CGSize? private var currentSize: CGSize? @@ -43,9 +43,7 @@ public final class ComponentHostView: UIView { self.isUpdating = true precondition(containerSize.width.isFinite) - precondition(containerSize.width < .greatestFiniteMagnitude) precondition(containerSize.height.isFinite) - precondition(containerSize.height < .greatestFiniteMagnitude) let componentView: UIView if let current = self.componentView { @@ -62,8 +60,9 @@ public final class ComponentHostView: UIView { if updateEnvironment { EnvironmentBuilder._environment = context.erasedEnvironment - let _ = maybeEnvironment() + let environmentResult = maybeEnvironment() EnvironmentBuilder._environment = nil + context.erasedEnvironment = environmentResult } let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() diff --git a/submodules/Components/Forms/CreditCardInputComponent/BUILD b/submodules/Components/Forms/CreditCardInputComponent/BUILD new file mode 100644 index 0000000000..3f2063bdbe --- /dev/null +++ b/submodules/Components/Forms/CreditCardInputComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CreditCardInputComponent", + module_name = "CreditCardInputComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Stripe:Stripe", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift b/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift new file mode 100644 index 0000000000..cc36130746 --- /dev/null +++ b/submodules/Components/Forms/CreditCardInputComponent/Sources/CreditCardInputComponent.swift @@ -0,0 +1,172 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import Stripe + +public final class CreditCardInputComponent: Component { + public enum DataType { + case cardNumber + case expirationDate + } + + public let dataType: DataType + public let text: String + public let textColor: UIColor + public let errorTextColor: UIColor + public let placeholder: String + public let placeholderColor: UIColor + public let updated: (String) -> Void + + public init( + dataType: DataType, + text: String, + textColor: UIColor, + errorTextColor: UIColor, + placeholder: String, + placeholderColor: UIColor, + updated: @escaping (String) -> Void + ) { + self.dataType = dataType + self.text = text + self.textColor = textColor + self.errorTextColor = errorTextColor + self.placeholder = placeholder + self.placeholderColor = placeholderColor + self.updated = updated + } + + public static func ==(lhs: CreditCardInputComponent, rhs: CreditCardInputComponent) -> Bool { + if lhs.dataType != rhs.dataType { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.errorTextColor != rhs.errorTextColor { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + return true + } + + public final class View: UIView, STPFormTextFieldDelegate, UITextFieldDelegate { + private let textField: STPFormTextField + + private var component: CreditCardInputComponent? + private let viewModel: STPPaymentCardTextFieldViewModel + + override init(frame: CGRect) { + self.textField = STPFormTextField(frame: CGRect()) + + self.viewModel = STPPaymentCardTextFieldViewModel() + + super.init(frame: frame) + + self.textField.backgroundColor = .clear + self.textField.keyboardType = .phonePad + + self.textField.formDelegate = self + self.textField.validText = true + + self.addSubview(self.textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func textFieldChanged(_ textField: UITextField) { + self.component?.updated(self.textField.text ?? "") + } + + public func formTextFieldDidBackspace(onEmpty formTextField: STPFormTextField) { + } + + public func formTextField(_ formTextField: STPFormTextField, modifyIncomingTextChange input: NSAttributedString) -> NSAttributedString { + guard let component = self.component else { + return input + } + + switch component.dataType { + case .cardNumber: + self.viewModel.cardNumber = input.string + return NSAttributedString(string: self.viewModel.cardNumber ?? "", attributes: self.textField.defaultTextAttributes) + case .expirationDate: + self.viewModel.rawExpiration = input.string + return NSAttributedString(string: self.viewModel.rawExpiration ?? "", attributes: self.textField.defaultTextAttributes) + } + } + + public func formTextFieldTextDidChange(_ textField: STPFormTextField) { + guard let component = self.component else { + return + } + + component.updated(self.textField.text ?? "") + + let state: STPCardValidationState + switch component.dataType { + case .cardNumber: + state = self.viewModel.validationState(for: .number) + case .expirationDate: + state = self.viewModel.validationState(for: .expiration) + } + self.textField.validText = true + switch state { + case .invalid: + self.textField.validText = false + case .incomplete: + break + case .valid: + break + @unknown default: + break + } + } + + func update(component: CreditCardInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + switch component.dataType { + case .cardNumber: + self.textField.autoFormattingBehavior = .cardNumbers + case .expirationDate: + self.textField.autoFormattingBehavior = .expiration + } + + self.textField.font = UIFont.systemFont(ofSize: 17.0) + self.textField.defaultColor = component.textColor + self.textField.errorColor = .red + self.textField.placeholderColor = component.placeholderColor + + if self.textField.text != component.text { + self.textField.text = component.text + } + + self.textField.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.textField.font, textColor: component.placeholderColor) + + let size = CGSize(width: availableSize.width, height: 44.0) + + transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(), size: size), completion: nil) + + self.component = component + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/Components/Forms/PrefixSectionGroupComponent/BUILD b/submodules/Components/Forms/PrefixSectionGroupComponent/BUILD new file mode 100644 index 0000000000..14dd8f6f1f --- /dev/null +++ b/submodules/Components/Forms/PrefixSectionGroupComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PrefixSectionGroupComponent", + module_name = "PrefixSectionGroupComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift b/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift new file mode 100644 index 0000000000..f0c86926b1 --- /dev/null +++ b/submodules/Components/Forms/PrefixSectionGroupComponent/Sources/PrefixSectionGroupComponent.swift @@ -0,0 +1,194 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +public final class PrefixSectionGroupComponent: Component { + public final class Item: Equatable { + public let prefix: AnyComponentWithIdentity + public let content: AnyComponentWithIdentity + + public init(prefix: AnyComponentWithIdentity, content: AnyComponentWithIdentity) { + self.prefix = prefix + self.content = content + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.prefix != rhs.prefix { + return false + } + if lhs.content != rhs.content { + return false + } + + return true + } + } + + public let items: [Item] + public let backgroundColor: UIColor + public let separatorColor: UIColor + + public init( + items: [Item], + backgroundColor: UIColor, + separatorColor: UIColor + ) { + self.items = items + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + } + + public static func ==(lhs: PrefixSectionGroupComponent, rhs: PrefixSectionGroupComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.separatorColor != rhs.separatorColor { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: UIView + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var separatorViews: [UIView] = [] + + override init(frame: CGRect) { + self.backgroundView = UIView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.backgroundView.layer.cornerRadius = 10.0 + self.backgroundView.layer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PrefixSectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let spacing: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + + self.backgroundView.backgroundColor = component.backgroundColor + + var size = CGSize(width: availableSize.width, height: 0.0) + + var validIds: [AnyHashable] = [] + + var maxPrefixSize = CGSize() + var prefixItemSizes: [CGSize] = [] + for item in component.items { + validIds.append(item.prefix.id) + + let itemView: ComponentHostView + var itemTransition = transition + if let current = self.itemViews[item.prefix.id] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = ComponentHostView() + self.itemViews[item.prefix.id] = itemView + self.addSubview(itemView) + } + let itemSize = itemView.update( + transition: itemTransition, + component: item.prefix.component, + environment: {}, + containerSize: CGSize(width: size.width, height: .greatestFiniteMagnitude) + ) + prefixItemSizes.append(itemSize) + maxPrefixSize.width = max(maxPrefixSize.width, itemSize.width) + maxPrefixSize.height = max(maxPrefixSize.height, itemSize.height) + } + + var maxContentSize = CGSize() + var contentItemSizes: [CGSize] = [] + for item in component.items { + validIds.append(item.content.id) + + let itemView: ComponentHostView + var itemTransition = transition + if let current = self.itemViews[item.content.id] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = ComponentHostView() + self.itemViews[item.content.id] = itemView + self.addSubview(itemView) + } + let itemSize = itemView.update( + transition: itemTransition, + component: item.content.component, + environment: {}, + containerSize: CGSize(width: size.width - maxPrefixSize.width - sideInset - spacing, height: .greatestFiniteMagnitude) + ) + contentItemSizes.append(itemSize) + maxContentSize.width = max(maxContentSize.width, itemSize.width) + maxContentSize.height = max(maxContentSize.height, itemSize.height) + } + + for i in 0 ..< component.items.count { + let itemSize = CGSize(width: size.width, height: max(prefixItemSizes[i].height, contentItemSizes[i].height)) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize) + + let prefixView = itemViews[component.items[i].prefix.id]! + let contentView = itemViews[component.items[i].content.id]! + + prefixView.frame = CGRect(origin: CGPoint(x: sideInset, y: itemFrame.minY + floor((itemFrame.height - prefixItemSizes[i].height) / 2.0)), size: prefixItemSizes[i]) + + contentView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset + maxPrefixSize.width + spacing, y: itemFrame.minY + floor((itemFrame.height - contentItemSizes[i].height) / 2.0)), size: contentItemSizes[i]) + + size.height += itemSize.height + + if i != component.items.count - 1 { + let separatorView: UIView + if self.separatorViews.count > i { + separatorView = self.separatorViews[i] + } else { + separatorView = UIView() + self.separatorViews.append(separatorView) + self.addSubview(separatorView) + } + separatorView.backgroundColor = component.separatorColor + separatorView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset, y: itemFrame.maxY), size: CGSize(width: itemFrame.width - sideInset, height: UIScreenPixel)) + } + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + + if self.separatorViews.count > component.items.count - 1 { + for i in (component.items.count - 1) ..< self.separatorViews.count { + self.separatorViews[i].removeFromSuperview() + } + self.separatorViews.removeSubrange((component.items.count - 1) ..< self.separatorViews.count) + } + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size), completion: nil) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/Components/Forms/TextInputComponent/BUILD b/submodules/Components/Forms/TextInputComponent/BUILD new file mode 100644 index 0000000000..36410527b9 --- /dev/null +++ b/submodules/Components/Forms/TextInputComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TextInputComponent", + module_name = "TextInputComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift b/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift new file mode 100644 index 0000000000..b1d14eacf4 --- /dev/null +++ b/submodules/Components/Forms/TextInputComponent/Sources/TextInputComponent.swift @@ -0,0 +1,86 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +public final class TextInputComponent: Component { + public let text: String + public let textColor: UIColor + public let placeholder: String + public let placeholderColor: UIColor + public let updated: (String) -> Void + + public init( + text: String, + textColor: UIColor, + placeholder: String, + placeholderColor: UIColor, + updated: @escaping (String) -> Void + ) { + self.text = text + self.textColor = textColor + self.placeholder = placeholder + self.placeholderColor = placeholderColor + self.updated = updated + } + + public static func ==(lhs: TextInputComponent, rhs: TextInputComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + return true + } + + public final class View: UITextField, UITextFieldDelegate { + private var component: TextInputComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.delegate = self + self.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func textFieldChanged(_ textField: UITextField) { + self.component?.updated(self.text ?? "") + } + + func update(component: TextInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.font = UIFont.systemFont(ofSize: 17.0) + self.textColor = component.textColor + + if self.text != component.text { + self.text = component.text + } + + self.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.font, textColor: component.placeholderColor) + + let size = CGSize(width: availableSize.width, height: 44.0) + + self.component = component + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/Components/MultilineTextComponent/BUILD b/submodules/Components/MultilineTextComponent/BUILD index 100013f1bb..6d3a4983a3 100644 --- a/submodules/Components/MultilineTextComponent/BUILD +++ b/submodules/Components/MultilineTextComponent/BUILD @@ -12,9 +12,7 @@ swift_library( deps = [ "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AccountContext:AccountContext", - "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/Markdown:Markdown", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 1e99f867c5..2aa41374a6 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -2,21 +2,27 @@ import Foundation import UIKit import ComponentFlow import Display +import Markdown public final class MultilineTextComponent: Component { - public let text: NSAttributedString + public enum TextContent: Equatable { + case plain(NSAttributedString) + case markdown(text: String, attributes: MarkdownAttributes) + } + + public let text: TextContent public let horizontalAlignment: NSTextAlignment public let verticalAlignment: TextVerticalAlignment - public var truncationType: CTLineTruncationType - public var maximumNumberOfLines: Int - public var lineSpacing: CGFloat - public var cutout: TextNodeCutout? - public var insets: UIEdgeInsets - public var textShadowColor: UIColor? - public var textStroke: (UIColor, CGFloat)? + public let truncationType: CTLineTruncationType + public let maximumNumberOfLines: Int + public let lineSpacing: CGFloat + public let cutout: TextNodeCutout? + public let insets: UIEdgeInsets + public let textShadowColor: UIColor? + public let textStroke: (UIColor, CGFloat)? public init( - text: NSAttributedString, + text: TextContent, horizontalAlignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, truncationType: CTLineTruncationType = .end, @@ -40,7 +46,7 @@ public final class MultilineTextComponent: Component { } public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool { - if !lhs.text.isEqual(to: rhs.text) { + if lhs.text != rhs.text { return false } if lhs.horizontalAlignment != rhs.horizontalAlignment { @@ -89,9 +95,17 @@ public final class MultilineTextComponent: Component { public final class View: TextView { public func update(component: MultilineTextComponent, availableSize: CGSize) -> CGSize { + let attributedString: NSAttributedString + switch component.text { + case let .plain(string): + attributedString = string + case let .markdown(text, attributes): + attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes) + } + let makeLayout = TextView.asyncLayout(self) let (layout, apply) = makeLayout(TextNodeLayoutArguments( - attributedString: component.text, + attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: component.maximumNumberOfLines, truncationType: component.truncationType, diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 54a6a1d0b1..adca84a898 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -114,11 +114,14 @@ open class ViewControllerComponentContainer: ViewController { } } + public final class AnimateInTransition { + } + public final class Node: ViewControllerTracingNode { private var presentationData: PresentationData private weak var controller: ViewControllerComponentContainer? - private let component: AnyComponent + private var component: AnyComponent private let theme: PresentationTheme? public let hostView: ComponentHostView @@ -164,7 +167,7 @@ open class ViewControllerComponentContainer: ViewController { transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) } - func updateIsVisible(isVisible: Bool) { + func updateIsVisible(isVisible: Bool, animated: Bool) { if self.currentIsVisible == isVisible { return } @@ -173,7 +176,16 @@ open class ViewControllerComponentContainer: ViewController { guard let currentLayout = self.currentLayout else { return } - self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? Transition(animation: .none).withUserData(AnimateInTransition()) : .immediate) + } + + func updateComponent(component: AnyComponent, transition: Transition) { + self.component = component + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition) } } @@ -215,13 +227,13 @@ open class ViewControllerComponentContainer: ViewController { override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.node.updateIsVisible(isVisible: true) + self.node.updateIsVisible(isVisible: true, animated: true) } override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - self.node.updateIsVisible(isVisible: false) + self.node.updateIsVisible(isVisible: false, animated: false) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -231,4 +243,8 @@ open class ViewControllerComponentContainer: ViewController { self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } + + public func updateComponent(component: AnyComponent, transition: Transition) { + self.node.updateComponent(component: component, transition: transition) + } } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 7480733e0f..d067b2865d 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -12,15 +12,15 @@ import AccountContext import Markdown import TextFormat -class InviteLinkHeaderItem: ListViewItem, ItemListItem { - let context: AccountContext - let theme: PresentationTheme - let text: String - let animationName: String - let sectionId: ItemListSectionId - let linkAction: ((ItemListTextItemLinkAction) -> Void)? +public class InviteLinkHeaderItem: ListViewItem, ItemListItem { + public let context: AccountContext + public let theme: PresentationTheme + public let text: String + public let animationName: String + public let sectionId: ItemListSectionId + public let linkAction: ((ItemListTextItemLinkAction) -> Void)? - init(context: AccountContext, theme: PresentationTheme, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + public init(context: AccountContext, theme: PresentationTheme, text: String, animationName: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { self.context = context self.theme = theme self.text = text @@ -29,7 +29,7 @@ class InviteLinkHeaderItem: ListViewItem, ItemListItem { self.linkAction = linkAction } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = InviteLinkHeaderItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -45,7 +45,7 @@ class InviteLinkHeaderItem: ListViewItem, ItemListItem { } } - 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) { + public 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 { guard let nodeValue = node() as? InviteLinkHeaderItemNode else { assertionFailure() diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 1ce3ca2adb..5680cd0609 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -31,6 +31,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { let iconSize: CGSize? let iconPlacement: IconPlacement let title: String + let subtitle: String? let style: ItemListCheckboxItemStyle let color: ItemListCheckboxItemColor let textColor: TextColor @@ -40,12 +41,13 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { let action: () -> Void let deleteAction: (() -> Void)? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) { self.presentationData = presentationData self.icon = icon self.iconSize = iconSize self.iconPlacement = iconPlacement self.title = title + self.subtitle = subtitle self.style = style self.color = color self.textColor = textColor @@ -111,6 +113,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { private let imageNode: ASImageNode private let iconNode: ASImageNode private let titleNode: TextNode + private let subtitleNode: TextNode private var item: ItemListCheckboxItem? @@ -149,6 +152,11 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.contentMode = .left + self.subtitleNode.contentsScale = UIScreen.main.scale + self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -161,6 +169,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { self.contentContainerNode.addSubnode(self.imageNode) self.contentContainerNode.addSubnode(self.iconNode) self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.subtitleNode) self.addSubnode(self.activateArea) self.activateArea.activate = { [weak self] in @@ -171,6 +180,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { public func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let currentItem = self.item @@ -181,7 +191,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { case .left: leftInset += 62.0 case .right: - leftInset += 16.0 + leftInset += 0.0 } let iconInset: CGFloat = 62.0 @@ -195,8 +205,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { } let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let titleColor: UIColor + let subtitleColor: UIColor = item.presentationData.theme.list.itemSecondaryTextColor switch item.textColor { case .primary: titleColor = item.presentationData.theme.list.itemPrimaryTextColor @@ -206,10 +218,15 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle ?? "", font: subtitleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) + var contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) + if item.subtitle != nil { + contentSize.height += subtitleLayout.size.height + } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -257,6 +274,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { } let _ = titleApply() + let _ = subtitleApply() if let image = strongSelf.iconNode.image { switch item.style { @@ -313,6 +331,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY), size: subtitleLayout.size) if let icon = item.icon { let iconSize = item.iconSize ?? icon.size diff --git a/submodules/Markdown/Source/Markdown.swift b/submodules/Markdown/Source/Markdown.swift index 73a3334ca5..cd15d81899 100644 --- a/submodules/Markdown/Source/Markdown.swift +++ b/submodules/Markdown/Source/Markdown.swift @@ -4,7 +4,7 @@ import UIKit private let controlStartCharactersSet = CharacterSet(charactersIn: "[*") private let controlCharactersSet = CharacterSet(charactersIn: "[]()*_-\\") -public final class MarkdownAttributeSet { +public final class MarkdownAttributeSet: Equatable { public let font: UIFont public let textColor: UIColor public let additionalAttributes: [String: Any] @@ -14,9 +14,19 @@ public final class MarkdownAttributeSet { self.textColor = textColor self.additionalAttributes = additionalAttributes } + + public static func ==(lhs: MarkdownAttributeSet, rhs: MarkdownAttributeSet) -> Bool { + if !lhs.font.isEqual(rhs.font) { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + return true + } } -public final class MarkdownAttributes { +public final class MarkdownAttributes: Equatable { public let body: MarkdownAttributeSet public let bold: MarkdownAttributeSet public let link: MarkdownAttributeSet @@ -28,6 +38,19 @@ public final class MarkdownAttributes { self.bold = bold self.linkAttribute = linkAttribute } + + public static func ==(lhs: MarkdownAttributes, rhs: MarkdownAttributes) -> Bool { + if lhs.body != rhs.body { + return false + } + if lhs.bold != rhs.bold { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } } public func escapedPlaintextForMarkdown(_ string: String) -> String { diff --git a/submodules/PaymentMethodUI/BUILD b/submodules/PaymentMethodUI/BUILD new file mode 100644 index 0000000000..ceeeb902d4 --- /dev/null +++ b/submodules/PaymentMethodUI/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PaymentMethodUI", + module_name = "PaymentMethodUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent", + "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/UndoPanelComponent:UndoPanelComponent", + "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", + "//submodules/AccountContext:AccountContext", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/Components/Forms/PrefixSectionGroupComponent:PrefixSectionGroupComponent", + "//submodules/Components/Forms/TextInputComponent:TextInputComponent", + "//submodules/Markdown:Markdown", + "//submodules/InviteLinksUI:InviteLinksUI", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/UndoUI:UndoUI", + "//submodules/Stripe:Stripe", + "//submodules/Components/Forms/CreditCardInputComponent:CreditCardInputComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PaymentMethodUI/Sources/AddPaymentMethodScheetScreen.swift b/submodules/PaymentMethodUI/Sources/AddPaymentMethodScheetScreen.swift new file mode 100644 index 0000000000..aced0021f1 --- /dev/null +++ b/submodules/PaymentMethodUI/Sources/AddPaymentMethodScheetScreen.swift @@ -0,0 +1,416 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import AnimatedStickerComponent +import SolidRoundedButtonComponent +import MultilineTextComponent +import PresentationDataUtils + +public final class SheetComponentEnvironment: Equatable { + public let isDisplaying: Bool + public let dismiss: () -> Void + + public init(isDisplaying: Bool, dismiss: @escaping () -> Void) { + self.isDisplaying = isDisplaying + self.dismiss = dismiss + } + + public static func ==(lhs: SheetComponentEnvironment, rhs: SheetComponentEnvironment) -> Bool { + if lhs.isDisplaying != rhs.isDisplaying { + return false + } + return true + } +} + +public final class SheetComponent: Component { + public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) + + public let content: AnyComponent + public let backgroundColor: UIColor + public let animateOut: ActionSlot> + + public init(content: AnyComponent, backgroundColor: UIColor, animateOut: ActionSlot>) { + self.content = content + self.backgroundColor = backgroundColor + self.animateOut = animateOut + } + + public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.animateOut != rhs.animateOut { + return false + } + + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let scrollView: UIScrollView + private let backgroundView: UIView + private let contentView: ComponentHostView + + private var previousIsDisplaying: Bool = false + private var dismiss: (() -> Void)? + + override init(frame: CGRect) { + self.dimView = UIView() + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + + self.scrollView = UIScrollView() + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceVertical = true + + self.backgroundView = UIView() + self.backgroundView.layer.cornerRadius = 10.0 + self.backgroundView.layer.masksToBounds = true + + self.contentView = ComponentHostView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + + self.scrollView.addSubview(self.backgroundView) + self.scrollView.addSubview(self.contentView) + self.addSubview(self.scrollView) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss?() + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + } + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) { + return self.dimView + } + + return super.hitTest(point, with: event) + } + + private func animateOut(completion: @escaping () -> Void) { + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.bounds.height - self.scrollView.contentInset.top), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + } + + func update(component: SheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + component.animateOut.connect { [weak self] completion in + guard let strongSelf = self else { + return + } + strongSelf.animateOut { + completion(Void()) + } + } + + if self.backgroundView.backgroundColor != component.backgroundColor { + self.backgroundView.backgroundColor = component.backgroundColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) + + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironmentType.self] + }, + containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) + ) + + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) + self.scrollView.contentSize = contentSize + self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height), left: 0.0, bottom: 0.0, right: 0.0) + + if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.scrollView.layer.animatePosition(from: CGPoint(x: 0.0, y: availableSize.height - self.scrollView.contentInset.top), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: nil) + } + self.previousIsDisplaying = environment[SheetComponentEnvironment.self].value.isDisplaying + + self.dismiss = environment[SheetComponentEnvironment.self].value.dismiss + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class AddPaymentMethodSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let action: () -> Void + private let dismiss: () -> Void + + init(context: AccountContext, action: @escaping () -> Void, dismiss: @escaping () -> Void) { + self.context = context + self.action = action + self.dismiss = dismiss + } + + static func ==(lhs: AddPaymentMethodSheetContent, rhs: AddPaymentMethodSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + static var body: Body { + let animation = Child(AnimatedStickerComponent.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let actionButton = Child(SolidRoundedButtonComponent.self) + let cancelButton = Child(Button.self) + + return { context in + let sideInset: CGFloat = 40.0 + let buttonSideInset: CGFloat = 16.0 + + let environment = context.environment[EnvironmentType.self].value + let action = context.component.action + let dismiss = context.component.dismiss + + let animation = animation.update( + component: AnimatedStickerComponent( + account: context.component.context.account, + animation: AnimatedStickerComponent.Animation( + source: .bundle(name: "CreateStream"), + loop: true + ), + size: CGSize(width: 138.0, height: 138.0) + ), + availableSize: CGSize(width: 138.0, height: 138.0), + transition: context.transition + ) + + //TODO:localize + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Payment Method", font: UIFont.boldSystemFont(ofSize: 17.0), textColor: .black)), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + + //TODO:localize + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Add your debit or credit card to buy goods and services on Telegram.", font: UIFont.systemFont(ofSize: 15.0), textColor: .gray)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + + //TODO:localize + let actionButton = actionButton.update( + component: SolidRoundedButtonComponent( + title: "Add Payment Method", + theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: true, + action: { + dismiss() + action() + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonSideInset * 2.0, height: 50.0), + transition: context.transition + ) + + //TODO:localize + let cancelButton = cancelButton.update( + component: Button( + content: AnyComponent( + Text( + text: "Cancel", + font: UIFont.systemFont(ofSize: 17.0), + color: environment.theme.list.itemAccentColor + ) + ), + action: { + dismiss() + } + ).minSize(CGSize(width: context.availableSize.width - buttonSideInset * 2.0, height: 50.0)), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - buttonSideInset * 2.0, height: 50.0), + transition: context.transition + ) + + var size = CGSize(width: context.availableSize.width, height: 24.0) + + context.add(animation + .position(CGPoint(x: size.width / 2.0, y: size.height + animation.size.height / 2.0)) + ) + size.height += animation.size.height + size.height += 16.0 + + context.add(title + .position(CGPoint(x: size.width / 2.0, y: size.height + title.size.height / 2.0)) + ) + size.height += title.size.height + size.height += 16.0 + + context.add(text + .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) + ) + size.height += text.size.height + size.height += 40.0 + + context.add(actionButton + .position(CGPoint(x: size.width / 2.0, y: size.height + actionButton.size.height / 2.0)) + ) + size.height += actionButton.size.height + size.height += 8.0 + + context.add(cancelButton + .position(CGPoint(x: size.width / 2.0, y: size.height + cancelButton.size.height / 2.0)) + ) + size.height += cancelButton.size.height + + size.height += 8.0 + max(environment.safeInsets.bottom, 15.0) + + return size + } + } +} + +private final class AddPaymentMethodSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let action: () -> Void + + init(context: AccountContext, action: @escaping () -> Void) { + self.context = context + self.action = action + } + + static func ==(lhs: AddPaymentMethodSheetComponent, rhs: AddPaymentMethodSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(AddPaymentMethodSheetContent( + context: context.component.context, + action: context.component.action, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .white, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class AddPaymentMethodSheetScreen: ViewControllerComponentContainer { + public init(context: AccountContext, action: @escaping () -> Void) { + super.init(context: context, component: AddPaymentMethodSheetComponent(context: context, action: action), navigationBarAppearance: .none) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift b/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift new file mode 100644 index 0000000000..fc21792866 --- /dev/null +++ b/submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift @@ -0,0 +1,449 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import AnimatedStickerComponent +import SolidRoundedButtonComponent +import MultilineTextComponent +import PresentationDataUtils +import PrefixSectionGroupComponent +import TextInputComponent +import CreditCardInputComponent +import Markdown + +public final class ScrollChildEnvironment: Equatable { + public let insets: UIEdgeInsets + + public init(insets: UIEdgeInsets) { + self.insets = insets + } + + public static func ==(lhs: ScrollChildEnvironment, rhs: ScrollChildEnvironment) -> Bool { + if lhs.insets != rhs.insets { + return false + } + + return true + } +} + +public final class ScrollComponent: Component { + public typealias EnvironmentType = ChildEnvironment + + public let content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)> + public let contentInsets: UIEdgeInsets + + public init( + content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>, + contentInsets: UIEdgeInsets + ) { + self.content = content + self.contentInsets = contentInsets + } + + public static func ==(lhs: ScrollComponent, rhs: ScrollComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.contentInsets != rhs.contentInsets { + return false + } + + return true + } + + public final class View: UIScrollView { + private let contentView: ComponentHostView<(ChildEnvironment, ScrollChildEnvironment)> + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + + super.init(frame: frame) + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.contentInsetAdjustmentBehavior = .never + } + + self.addSubview(self.contentView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironment.self] + ScrollChildEnvironment(insets: component.contentInsets) + }, + containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) + ) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + + self.contentSize = contentSize + self.scrollIndicatorInsets = component.contentInsets + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private struct CardEntryModel: Equatable { + var number: String + var name: String + var expiration: String + var code: String +} + +private extension CardEntryModel { + var isValid: Bool { + if self.number.count != 4 * 4 { + return false + } + if self.name.isEmpty { + return false + } + if self.expiration.isEmpty { + return false + } + if self.code.count != 3 { + return false + } + return true + } +} + +private final class PaymentCardEntryScreenContentComponent: CombinedComponent { + typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) + + let context: AccountContext + let model: CardEntryModel + let updateModelKey: (WritableKeyPath, String) -> Void + + init(context: AccountContext, model: CardEntryModel, updateModelKey: @escaping (WritableKeyPath, String) -> Void) { + self.context = context + self.model = model + self.updateModelKey = updateModelKey + } + + static func ==(lhs: PaymentCardEntryScreenContentComponent, rhs: PaymentCardEntryScreenContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.model != rhs.model { + return false + } + + return true + } + + static var body: Body { + let animation = Child(AnimatedStickerComponent.self) + let text = Child(MultilineTextComponent.self) + let inputSection = Child(PrefixSectionGroupComponent.self) + let infoText = Child(MultilineTextComponent.self) + + return { context in + let sideInset: CGFloat = 16.0 + + let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let updateModelKey = context.component.updateModelKey + + var size = CGSize(width: context.availableSize.width, height: scrollEnvironment.insets.top) + size.height += 18.0 + + let animation = animation.update( + component: AnimatedStickerComponent( + account: context.component.context.account, + animation: AnimatedStickerComponent.Animation( + source: .bundle(name: "CreateStream"), + loop: true + ), + size: CGSize(width: 84.0, height: 84.0) + ), + availableSize: CGSize(width: 84.0, height: 84.0), + transition: context.transition + ) + + context.add(animation + .position(CGPoint(x: size.width / 2.0, y: size.height + animation.size.height / 2.0)) + ) + size.height += animation.size.height + size.height += 35.0 + + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Enter your card information or take a photo.", font: UIFont.systemFont(ofSize: 13.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)) + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), + transition: context.transition + ) + + context.add(text + .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) + ) + size.height += text.size.height + size.height += 32.0 + + let inputSection = inputSection.update( + component: PrefixSectionGroupComponent( + items: [ + PrefixSectionGroupComponent.Item( + prefix: AnyComponentWithIdentity( + id: "numberLabel", + component: AnyComponent(Text(text: "Number", font: Font.regular(17.0), color: environment.theme.list.itemPrimaryTextColor)) + ), + content: AnyComponentWithIdentity( + id: "numberInput", + component: AnyComponent(CreditCardInputComponent( + dataType: .cardNumber, + text: context.component.model.number, + textColor: environment.theme.list.itemPrimaryTextColor, + errorTextColor: environment.theme.list.itemDestructiveColor, + placeholder: "Card Number", + placeholderColor: environment.theme.list.itemPlaceholderTextColor, + updated: { value in + updateModelKey(\.number, value) + } + )) + ) + ), + PrefixSectionGroupComponent.Item( + prefix: AnyComponentWithIdentity( + id: "nameLabel", + component: AnyComponent(Text(text: "Name", font: Font.regular(17.0), color: environment.theme.list.itemPrimaryTextColor)) + ), + content: AnyComponentWithIdentity( + id: "nameInput", + component: AnyComponent(TextInputComponent( + text: context.component.model.name, + textColor: environment.theme.list.itemPrimaryTextColor, + placeholder: "Cardholder", + placeholderColor: environment.theme.list.itemPlaceholderTextColor, + updated: { value in + updateModelKey(\.name, value) + } + )) + ) + ), + PrefixSectionGroupComponent.Item( + prefix: AnyComponentWithIdentity( + id: "expiresLabel", + component: AnyComponent(Text(text: "Expires", font: Font.regular(17.0), color: environment.theme.list.itemPrimaryTextColor)) + ), + content: AnyComponentWithIdentity( + id: "expiresInput", + component: AnyComponent(CreditCardInputComponent( + dataType: .expirationDate, + text: context.component.model.expiration, + textColor: environment.theme.list.itemPrimaryTextColor, + errorTextColor: environment.theme.list.itemDestructiveColor, + placeholder: "MM/YY", + placeholderColor: environment.theme.list.itemPlaceholderTextColor, + updated: { value in + updateModelKey(\.expiration, value) + } + )) + ) + ), + PrefixSectionGroupComponent.Item( + prefix: AnyComponentWithIdentity( + id: "cvvLabel", + component: AnyComponent(Text(text: "CVV", font: Font.regular(17.0), color: environment.theme.list.itemPrimaryTextColor)) + ), + content: AnyComponentWithIdentity( + id: "cvvInput", + component: AnyComponent(TextInputComponent( + text: context.component.model.code, + textColor: environment.theme.list.itemPrimaryTextColor, + placeholder: "123", + placeholderColor: environment.theme.list.itemPlaceholderTextColor, + updated: { value in + updateModelKey(\.code, value) + } + )) + ) + ) + ], + backgroundColor: environment.theme.list.itemBlocksBackgroundColor, + separatorColor: environment.theme.list.itemBlocksSeparatorColor + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(inputSection + .position(CGPoint(x: size.width / 2.0, y: size.height + inputSection.size.height / 2.0)) + ) + size.height += inputSection.size.height + size.height += 8.0 + + let body = MarkdownAttributeSet(font: UIFont.systemFont(ofSize: 13.0), textColor: environment.theme.list.freeTextColor) + let link = MarkdownAttributeSet(font: UIFont.systemFont(ofSize: 13.0), textColor: environment.theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber]) + let attributes = MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in + return nil + }) + let infoText = infoText.update( + component: MultilineTextComponent( + text: .markdown(text: "By adding a card, you agree to the [Terms of Service](terms).", attributes: attributes) + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), + transition: context.transition + ) + context.add(infoText + .position(CGPoint(x: sideInset + sideInset + infoText.size.width / 2.0, y: size.height + infoText.size.height / 2.0)) + ) + size.height += text.size.height + + size.height += scrollEnvironment.insets.bottom + + return size + } + } +} + +private final class PaymentCardEntryScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let model: CardEntryModel + let updateModelKey: (WritableKeyPath, String) -> Void + + init(context: AccountContext, model: CardEntryModel, updateModelKey: @escaping(WritableKeyPath, String) -> Void) { + self.context = context + self.model = model + self.updateModelKey = updateModelKey + } + + static func ==(lhs: PaymentCardEntryScreenComponent, rhs: PaymentCardEntryScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.model != rhs.model { + return false + } + + return true + } + + static var body: Body { + let background = Child(Rectangle.self) + let scrollContent = Child(ScrollComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self].value + + let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition) + + let scrollContent = scrollContent.update( + component: ScrollComponent( + content: AnyComponent(PaymentCardEntryScreenContentComponent( + context: context.component.context, + model: context.component.model, + updateModelKey: context.component.updateModelKey + )), + contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) + ), + environment: { environment }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + context.add(scrollContent + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class PaymentCardEntryScreen: ViewControllerComponentContainer { + public struct EnteredCardInfo: Equatable { + public var id: UInt64 + public var number: String + public var name: String + public var expiration: String + public var code: String + } + + private let context: AccountContext + private let completion: (EnteredCardInfo) -> Void + + private var doneItem: UIBarButtonItem? + + private var model: CardEntryModel + + public init(context: AccountContext, completion: @escaping (EnteredCardInfo) -> Void) { + self.context = context + self.completion = completion + + self.model = CardEntryModel(number: "", name: "", expiration: "", code: "") + + var updateModelKeyImpl: ((WritableKeyPath, String) -> Void)? + + super.init(context: context, component: PaymentCardEntryScreenComponent(context: context, model: self.model, updateModelKey: { key, value in + updateModelKeyImpl?(key, value) + }), navigationBarAppearance: .transparent) + + //TODO:localize + self.title = "Add Payment Method" + + //TODO:localize + self.doneItem = UIBarButtonItem(title: "Add", style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.setRightBarButton(self.doneItem, animated: false) + self.doneItem?.isEnabled = false + + self.navigationPresentation = .modal + + updateModelKeyImpl = { [weak self] key, value in + self?.updateModelKey(key: key, value: value) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss() + } + + @objc private func donePressed() { + self.dismiss(completion: nil) + self.completion(EnteredCardInfo(id: UInt64.random(in: 0 ... UInt64.max), number: self.model.number, name: self.model.name, expiration: self.model.expiration, code: self.model.code)) + } + + private func updateModelKey(key: WritableKeyPath, value: String) { + self.model[keyPath: key] = value + self.updateComponent(component: AnyComponent(PaymentCardEntryScreenComponent(context: self.context, model: self.model, updateModelKey: { [weak self] key, value in + self?.updateModelKey(key: key, value: value) + })), transition: .immediate) + + self.doneItem?.isEnabled = self.model.isValid + } + + override public func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift new file mode 100644 index 0000000000..a748cfd264 --- /dev/null +++ b/submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift @@ -0,0 +1,286 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import PresentationDataUtils +import TelegramStringFormatting +import UndoUI +import InviteLinksUI +import Stripe + +private final class PaymentMethodListScreenArguments { + let context: AccountContext + let addMethod: () -> Void + let deleteMethod: (UInt64) -> Void + let selectMethod: (UInt64) -> Void + + init(context: AccountContext, addMethod: @escaping () -> Void, deleteMethod: @escaping (UInt64) -> Void, selectMethod: @escaping (UInt64) -> Void) { + self.context = context + self.addMethod = addMethod + self.deleteMethod = deleteMethod + self.selectMethod = selectMethod + } +} + +private enum PaymentMethodListSection: Int32 { + case header + case methods +} + +private enum InviteLinksListEntry: ItemListNodeEntry { + case header(String) + case methodsHeader(String) + case addMethod(String) + case item(index: Int, info: PaymentCardEntryScreen.EnteredCardInfo, isSelected: Bool) + + var section: ItemListSectionId { + switch self { + case .header: + return PaymentMethodListSection.header.rawValue + case .methodsHeader, .addMethod, .item: + return PaymentMethodListSection.methods.rawValue + } + } + + var sortId: Int { + switch self { + case .header: + return 0 + case .methodsHeader: + return 1 + case .addMethod: + return 2 + case let .item(index, _, _): + return 10 + index + } + } + + var stableId: UInt64 { + switch self { + case .header: + return 0 + case .methodsHeader: + return 1 + case .addMethod: + return 2 + case let .item(_, item, _): + return item.id + } + } + + static func ==(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { + switch lhs { + case let .header(lhsText): + if case let .header(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .methodsHeader(lhsText): + if case let .methodsHeader(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .addMethod(lhsText): + if case let .addMethod(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .item(lhsIndex, lhsItem, lhsIsSelected): + if case let .item(rhsIndex, rhsItem, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsItem == rhsItem, lhsIsSelected == rhsIsSelected { + return true + } else { + return false + } + } + } + + static func <(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PaymentMethodListScreenArguments + switch self { + case let .header(text): + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "Invite", sectionId: self.section) + case let .methodsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .addMethod(text): + let icon = PresentationResourcesItemList.plusIconImage(presentationData.theme) + return ItemListCheckboxItem(presentationData: presentationData, icon: icon, iconSize: nil, iconPlacement: .check, title: text, style: .left, textColor: .accent, checked: false, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.addMethod() + }) + case let .item(_, info, isSelected): + return ItemListCheckboxItem( + presentationData: presentationData, + icon: STPPaymentCardTextField.brandImage(for: .masterCard), iconSize: nil, + iconPlacement: .default, + title: "•••• " + info.number.suffix(4), + subtitle: "Expires \(info.expiration)", + style: .right, + color: .accent, + textColor: .primary, + checked: isSelected, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.selectMethod(info.id) + }, + deleteAction: { + arguments.deleteMethod(info.id) + } + ) + } + } +} + +private func paymentMethodListScreenEntries(presentationData: PresentationData, state: PaymentMethodListScreenState) -> [InviteLinksListEntry] { + var entries: [InviteLinksListEntry] = [] + + entries.append(.header("Add your debit or credit card to buy goods and\nservices on Telegram.")) + + entries.append(.methodsHeader("PAYMENT METHOD")) + entries.append(.addMethod("Add Payment Method")) + + for item in state.items { + entries.append(.item(index: entries.count, info: item, isSelected: state.selectedId == item.id)) + } + + return entries +} + +private struct PaymentMethodListScreenState: Equatable { + var items: [PaymentCardEntryScreen.EnteredCardInfo] + var selectedId: UInt64? +} + +public func paymentMethodListScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, items: [PaymentCardEntryScreen.EnteredCardInfo]) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + + let _ = presentControllerImpl + let _ = presentInGlobalOverlayImpl + + var dismissTooltipsImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let initialState = PaymentMethodListScreenState(items: items, selectedId: items.first?.id) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((PaymentMethodListScreenState) -> PaymentMethodListScreenState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + let _ = updateState + + var getControllerImpl: (() -> ViewController?)? + let _ = getControllerImpl + + let arguments = PaymentMethodListScreenArguments( + context: context, + addMethod: { + pushControllerImpl?(PaymentCardEntryScreen(context: context, completion: { result in + updateState { state in + var state = state + + state.items.insert(result, at: 0) + state.selectedId = result.id + + return state + } + })) + }, + deleteMethod: { id in + updateState { state in + var state = state + + state.items.removeAll(where: { $0.id == id }) + if state.selectedId == id { + state.selectedId = state.items.first?.id + } + + return state + } + }, + selectMethod: { id in + updateState { state in + var state = state + + state.selectedId = id + + return state + } + } + ) + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get() + ) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Payment Method"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: paymentMethodListScreenEntries(presentationData: presentationData, state: state), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.willDisappear = { _ in + dismissTooltipsImpl?() + } + controller.didDisappear = { [weak controller] _ in + controller?.clearItemNodesHighlight(animated: true) + } + controller.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + } + } + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) + } + } + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window(.root), with: p) + } + } + presentInGlobalOverlayImpl = { [weak controller] c in + if let controller = controller { + controller.presentInGlobalOverlay(c) + } + } + getControllerImpl = { [weak controller] in + return controller + } + dismissTooltipsImpl = { [weak controller] in + controller?.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + controller?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + } + return controller +} diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index 9a8454e4ad..8f5178f2b7 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -216,7 +216,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let text = text.update( component: MultilineTextComponent( - text: NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center), + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -228,7 +228,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let bottomText = Condition(mode == .create) { bottomText.update( component: MultilineTextComponent( - text: NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center), + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -290,7 +290,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent if let credentials = context.state.credentials { let credentialsURLTitle = credentialsURLTitle.update( component: MultilineTextComponent( - text: NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left), + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), horizontalAlignment: .left, maximumNumberOfLines: 1 ), @@ -300,7 +300,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let credentialsKeyTitle = credentialsKeyTitle.update( component: MultilineTextComponent( - text: NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left), + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), horizontalAlignment: .left, maximumNumberOfLines: 1 ), @@ -310,7 +310,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let credentialsURLText = credentialsURLText.update( component: MultilineTextComponent( - text: NSAttributedString(string: credentials.url, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left), + text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left)), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 @@ -321,7 +321,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let credentialsKeyText = credentialsKeyText.update( component: MultilineTextComponent( - text: NSAttributedString(string: credentials.streamKey, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left), + text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left)), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 diff --git a/submodules/PremiumUI/Sources/LimitScreen.swift b/submodules/PremiumUI/Sources/LimitScreen.swift index c21329c43e..d98ec12c58 100644 --- a/submodules/PremiumUI/Sources/LimitScreen.swift +++ b/submodules/PremiumUI/Sources/LimitScreen.swift @@ -81,7 +81,7 @@ private final class LimitScreenComponent: CombinedComponent { let title = title.update( component: MultilineTextComponent( - text: NSAttributedString(string: "Limit Reached", font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center), + text: .plain(NSAttributedString(string: "Limit Reached", font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 1 ), @@ -108,7 +108,7 @@ private final class LimitScreenComponent: CombinedComponent { let text = text.update( component: MultilineTextComponent( - text: attributedText, + text: .plain(attributedText), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 56f591662a..d5e058b134 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -99,6 +99,7 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/FetchManagerImpl:FetchManagerImpl", "//submodules/ListMessageItem:ListMessageItem", + "//submodules/PaymentMethodUI:PaymentMethodUI", ], visibility = [ "//visibility:public", diff --git a/submodules/Stripe/Sources/STPFormTextField.h b/submodules/Stripe/PublicHeaders/Stripe/STPFormTextField.h similarity index 100% rename from submodules/Stripe/Sources/STPFormTextField.h rename to submodules/Stripe/PublicHeaders/Stripe/STPFormTextField.h diff --git a/submodules/Stripe/Sources/STPPaymentCardTextFieldViewModel.h b/submodules/Stripe/PublicHeaders/Stripe/STPPaymentCardTextFieldViewModel.h similarity index 100% rename from submodules/Stripe/Sources/STPPaymentCardTextFieldViewModel.h rename to submodules/Stripe/PublicHeaders/Stripe/STPPaymentCardTextFieldViewModel.h diff --git a/submodules/Stripe/PublicHeaders/Stripe/Stripe.h b/submodules/Stripe/PublicHeaders/Stripe/Stripe.h index 7b2d10cfb4..f8b4b16fbe 100644 --- a/submodules/Stripe/PublicHeaders/Stripe/Stripe.h +++ b/submodules/Stripe/PublicHeaders/Stripe/Stripe.h @@ -2,6 +2,7 @@ #import #import +#import #import #import #import diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 8ee1cfe900..7a8a00edae 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -277,6 +277,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } + dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } + dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } dict[860303448] = { return Api.InputMedia.parse_inputMediaDocument($0) } @@ -987,7 +989,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } - dict[378828315] = { return Api.payments.PaymentForm.parse_paymentForm($0) } + dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } + dict[-1340916937] = { return Api.payments.PaymentForm.parse_paymentForm($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } @@ -1280,6 +1283,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputGroupCall: _1.serialize(buffer, boxed) + case let _1 as Api.InputInvoice: + _1.serialize(buffer, boxed) case let _1 as Api.InputMedia: _1.serialize(buffer, boxed) case let _1 as Api.InputMessage: @@ -1740,6 +1745,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.BankCardData: _1.serialize(buffer, boxed) + case let _1 as Api.payments.ExportedInvoice: + _1.serialize(buffer, boxed) case let _1 as Api.payments.PaymentForm: _1.serialize(buffer, boxed) case let _1 as Api.payments.PaymentReceipt: diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index c0500fe797..0635fa8ff7 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -647,18 +647,57 @@ public extension Api.payments { } } public extension Api.payments { - enum PaymentForm: TypeConstructorDescription { - case paymentForm(flags: Int32, formId: Int64, botId: Int64, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) + enum ExportedInvoice: TypeConstructorDescription { + case exportedInvoice(url: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + case .exportedInvoice(let url): if boxed { - buffer.appendInt32(378828315) + buffer.appendInt32(-1362048039) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedInvoice(let url): + return ("exportedInvoice", [("url", String(describing: url))]) + } + } + + public static func parse_exportedInvoice(_ reader: BufferReader) -> ExportedInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.ExportedInvoice.exportedInvoice(url: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.payments { + enum PaymentForm: TypeConstructorDescription { + case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + if boxed { + buffer.appendInt32(-1340916937) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(formId, buffer: buffer, boxed: false) serializeInt64(botId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} invoice.serialize(buffer, true) serializeInt64(providerId, buffer: buffer, boxed: false) serializeString(url, buffer: buffer, boxed: false) @@ -677,8 +716,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): - return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))]) + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))]) } } @@ -689,45 +728,56 @@ public extension Api.payments { _2 = reader.readInt64() var _3: Int64? _3 = reader.readInt64() - var _4: Api.Invoice? + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.WebDocument? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _7: Api.Invoice? if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Invoice + _7 = Api.parse(reader, signature: signature) as? Api.Invoice } - var _5: Int64? - _5 = reader.readInt64() - var _6: String? - _6 = parseString(reader) - var _7: String? - if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } - var _8: Api.DataJSON? + var _8: Int64? + _8 = reader.readInt64() + var _9: String? + _9 = parseString(reader) + var _10: String? + if Int(_1!) & Int(1 << 4) != 0 {_10 = parseString(reader) } + var _11: Api.DataJSON? if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.DataJSON + _11 = Api.parse(reader, signature: signature) as? Api.DataJSON } } - var _9: Api.PaymentRequestedInfo? + var _12: Api.PaymentRequestedInfo? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + _12 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } - var _10: Api.PaymentSavedCredentials? + var _13: Api.PaymentSavedCredentials? if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _10 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials + _13 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials } } - var _11: [Api.User]? + var _14: [Api.User]? if let _ = reader.readInt32() { - _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil - let _c11 = _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, invoice: _4!, providerId: _5!, url: _6!, nativeProvider: _7, nativeParams: _8, savedInfo: _9, savedCredentials: _10, users: _11!) + let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 0) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 1) == 0) || _13 != nil + let _c14 = _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, savedInfo: _12, savedCredentials: _13, users: _14!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 83c3189443..9043835513 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -6192,6 +6192,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func exportInvoice(invoiceMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(261206117) + invoiceMedia.serialize(buffer, true) + return (FunctionDescription(name: "payments.exportInvoice", parameters: [("invoiceMedia", String(describing: invoiceMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ExportedInvoice? in + let reader = BufferReader(buffer) + var result: Api.payments.ExportedInvoice? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.ExportedInvoice + } + return result + }) + } +} public extension Api.functions.payments { static func getBankCardData(number: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6208,14 +6223,13 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func getPaymentForm(flags: Int32, peer: Api.InputPeer, msgId: Int32, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getPaymentForm(flags: Int32, invoice: Api.InputInvoice, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1976353651) + buffer.appendInt32(924093883) serializeInt32(flags, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) + invoice.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)} - return (FunctionDescription(name: "payments.getPaymentForm", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("themeParams", String(describing: themeParams))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentForm? in + return (FunctionDescription(name: "payments.getPaymentForm", parameters: [("flags", String(describing: flags)), ("invoice", String(describing: invoice)), ("themeParams", String(describing: themeParams))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentForm? in let reader = BufferReader(buffer) var result: Api.payments.PaymentForm? if let signature = reader.readInt32() { @@ -6257,18 +6271,17 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func sendPaymentForm(flags: Int32, formId: Int64, peer: Api.InputPeer, msgId: Int32, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(818134173) + buffer.appendInt32(755192367) serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(formId, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) + invoice.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeString(requestedInfoId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeString(shippingOptionId!, buffer: buffer, boxed: false)} credentials.serialize(buffer, true) if Int(flags) & Int(1 << 2) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "payments.sendPaymentForm", parameters: [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("requestedInfoId", String(describing: requestedInfoId)), ("shippingOptionId", String(describing: shippingOptionId)), ("credentials", String(describing: credentials)), ("tipAmount", String(describing: tipAmount))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in + return (FunctionDescription(name: "payments.sendPaymentForm", parameters: [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("invoice", String(describing: invoice)), ("requestedInfoId", String(describing: requestedInfoId)), ("shippingOptionId", String(describing: shippingOptionId)), ("credentials", String(describing: credentials)), ("tipAmount", String(describing: tipAmount))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in let reader = BufferReader(buffer) var result: Api.payments.PaymentResult? if let signature = reader.readInt32() { @@ -6279,14 +6292,13 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func validateRequestedInfo(flags: Int32, peer: Api.InputPeer, msgId: Int32, info: Api.PaymentRequestedInfo) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func validateRequestedInfo(flags: Int32, invoice: Api.InputInvoice, info: Api.PaymentRequestedInfo) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-619695760) + buffer.appendInt32(-1228345045) serializeInt32(flags, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) + invoice.serialize(buffer, true) info.serialize(buffer, true) - return (FunctionDescription(name: "payments.validateRequestedInfo", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("info", String(describing: info))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ValidatedRequestedInfo? in + return (FunctionDescription(name: "payments.validateRequestedInfo", parameters: [("flags", String(describing: flags)), ("invoice", String(describing: invoice)), ("info", String(describing: info))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ValidatedRequestedInfo? in let reader = BufferReader(buffer) var result: Api.payments.ValidatedRequestedInfo? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 7228fb0394..ea95c104cb 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -800,6 +800,68 @@ public extension Api { } } +public extension Api { + enum InputInvoice: TypeConstructorDescription { + case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) + case inputInvoiceSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + if boxed { + buffer.appendInt32(-977967015) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + break + case .inputInvoiceSlug(let slug): + if boxed { + buffer.appendInt32(-1020867857) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + return ("inputInvoiceMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]) + case .inputInvoiceSlug(let slug): + return ("inputInvoiceSlug", [("slug", String(describing: slug))]) + } + } + + public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoiceMessage(peer: _1!, msgId: _2!) + } + else { + return nil + } + } + public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceSlug(slug: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputMedia: TypeConstructorDescription { case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 611b2ed3ca..951ffad732 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -286,7 +286,7 @@ final class MediaStreamVideoComponent: Component { let noSignalSize = noSignalView.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: NSAttributedString(string: component.isAdmin ? presentationData.strings.LiveStream_NoSignalAdminText : presentationData.strings.LiveStream_NoSignalUserText(component.peerTitle).string, font: Font.regular(16.0), textColor: .white, paragraphAlignment: .center), + text: .plain(NSAttributedString(string: component.isAdmin ? presentationData.strings.LiveStream_NoSignalAdminText : presentationData.strings.LiveStream_NoSignalUserText(component.peerTitle).string, font: Font.regular(16.0), textColor: .white, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 2ec581e49c..9dc91627d3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -4,6 +4,10 @@ import MtProtoKit import SwiftSignalKit import TelegramApi +public enum BotPaymentInvoiceSource { + case message(MessageId) + case slug(String) +} public struct BotPaymentInvoiceFields: OptionSet { public var rawValue: Int32 @@ -173,15 +177,70 @@ extension BotPaymentRequestedInfo { } } -func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, messageId: MessageId, themeParams: [String: Any]?) -> Signal { - return postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) +func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal { + return postbox.transaction { transaction -> Api.InputInvoice? in + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) + case let .slug(slug): + return .inputInvoiceSlug(slug: slug) + } } |> castError(BotPaymentFormRequestError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { + |> mapToSignal { invoice -> Signal in + guard let invoice = invoice else { return .fail(.generic) } + + let flags: Int32 = 0 + + return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: nil)) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> TelegramMediaInvoice in + switch result { + case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _): + let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) + + var parsedFlags = TelegramMediaInvoiceFlags() + if parsedInvoice.isTest { + parsedFlags.insert(.isTest) + } + if parsedInvoice.requestedFields.contains(.shippingAddress) { + parsedFlags.insert(.shippingAddressRequested) + } + + return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", flags: parsedFlags) + } + } + |> mapError { _ -> BotPaymentFormRequestError in } + } + } +} + +func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal { + return postbox.transaction { transaction -> Api.InputInvoice? in + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) + case let .slug(slug): + return .inputInvoiceSlug(slug: slug) + } + } + |> castError(BotPaymentFormRequestError.self) + |> mapToSignal { invoice -> Signal in + guard let invoice = invoice else { + return .fail(.generic) + } + var flags: Int32 = 0 var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { @@ -191,14 +250,18 @@ func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, messageId flags |= 1 << 0 } - return network.request(Api.functions.payments.getPaymentForm(flags: flags, peer: inputPeer, msgId: messageId.id, themeParams: serializedThemeParams)) + return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: serializedThemeParams)) |> `catch` { _ -> Signal in return .fail(.generic) } |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> BotPaymentForm in switch result { - case let .paymentForm(flags, id, botId, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers): + case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers): + let _ = title + let _ = description + let _ = photo + var peers: [Peer] = [] for user in apiUsers { let parsed = TelegramUser(user: user) @@ -268,13 +331,21 @@ extension BotPaymentShippingOption { } } -func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) +func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { + return account.postbox.transaction { transaction -> Api.InputInvoice? in + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) + case let .slug(slug): + return .inputInvoiceSlug(slug: slug) + } } |> castError(ValidateBotPaymentFormError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { + |> mapToSignal { invoice -> Signal in + guard let invoice = invoice else { return .fail(.generic) } @@ -297,7 +368,7 @@ func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, messageI infoFlags |= (1 << 3) apiShippingAddress = .postAddress(streetLine1: address.streetLine1, streetLine2: address.streetLine2, city: address.city, state: address.state, countryIso2: address.countryIso2, postCode: address.postCode) } - return account.network.request(Api.functions.payments.validateRequestedInfo(flags: flags, peer: inputPeer, msgId: messageId.id, info: .paymentRequestedInfo(flags: infoFlags, name: formInfo.name, phone: formInfo.phone, email: formInfo.email, shippingAddress: apiShippingAddress))) + return account.network.request(Api.functions.payments.validateRequestedInfo(flags: flags, invoice: invoice, info: .paymentRequestedInfo(flags: infoFlags, name: formInfo.name, phone: formInfo.phone, email: formInfo.email, shippingAddress: apiShippingAddress))) |> mapError { error -> ValidateBotPaymentFormError in if error.errorDescription == "SHIPPING_NOT_AVAILABLE" { return .shippingNotAvailable @@ -346,16 +417,24 @@ public enum SendBotPaymentResult { case externalVerificationRequired(url: String) } -func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) +func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { + return account.postbox.transaction { transaction -> Api.InputInvoice? in + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) + case let .slug(slug): + return .inputInvoiceSlug(slug: slug) + } } |> castError(SendBotPaymentFormError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { + |> mapToSignal { invoice -> Signal in + guard let invoice = invoice else { return .fail(.generic) } - + let apiCredentials: Api.InputPaymentCredentials switch credentials { case let .generic(data, saveOnServer): @@ -379,7 +458,8 @@ func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId if tipAmount != nil { flags |= (1 << 2) } - return account.network.request(Api.functions.payments.sendPaymentForm(flags: flags, formId: formId, peer: inputPeer, msgId: messageId.id, requestedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, credentials: apiCredentials, tipAmount: tipAmount)) + + return account.network.request(Api.functions.payments.sendPaymentForm(flags: flags, formId: formId, invoice: invoice, requestedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, credentials: apiCredentials, tipAmount: tipAmount)) |> map { result -> SendBotPaymentResult in switch result { case let .paymentResult(updates): @@ -392,10 +472,15 @@ func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId if case .paymentSent = action.action { for attribute in message.attributes { if let reply = attribute as? ReplyMessageAttribute { - if reply.messageId == messageId { - if case let .Id(id) = message.id { - receiptMessageId = id + switch source { + case let .message(messageId): + if reply.messageId == messageId { + if case let .Id(id) = message.id { + receiptMessageId = id + } } + case .slug: + break } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 7bde71b986..c953688813 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -12,17 +12,21 @@ public extension TelegramEngine { public func getBankCardInfo(cardNumber: String) -> Signal { return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber) } - - public func fetchBotPaymentForm(messageId: MessageId, themeParams: [String: Any]?) -> Signal { - return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, messageId: messageId, themeParams: themeParams) + + public func fetchBotPaymentInvoice(source: BotPaymentInvoiceSource) -> Signal { + return _internal_fetchBotPaymentInvoice(postbox: self.account.postbox, network: self.account.network, source: source) + } + + public func fetchBotPaymentForm(source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal { + return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, source: source, themeParams: themeParams) + } + + public func validateBotPaymentForm(saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { + return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, source: source, formInfo: formInfo) } - public func validateBotPaymentForm(saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal { - return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, messageId: messageId, formInfo: formInfo) - } - - public func sendBotPaymentForm(messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { - return _internal_sendBotPaymentForm(account: self.account, messageId: messageId, formId: formId, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) + public func sendBotPaymentForm(source: BotPaymentInvoiceSource, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { + return _internal_sendBotPaymentForm(account: self.account, formId: formId, source: source, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) } public func requestBotPaymentReceipt(messageId: MessageId) -> Signal { diff --git a/submodules/TelegramUI/Resources/PATTERN_static.svg b/submodules/TelegramUI/Resources/PATTERN_static.svg new file mode 100644 index 0000000000..d10eda2d53 --- /dev/null +++ b/submodules/TelegramUI/Resources/PATTERN_static.svg @@ -0,0 +1,1549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs b/submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs new file mode 100644 index 0000000000000000000000000000000000000000..955000e7508a766bd7cd3a8c0b16ed1920b9d5ae GIT binary patch literal 16651 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 aa)6tWcet5v4*U1RZ~q^8-B5gfzySdDv4HOY literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs b/submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs new file mode 100644 index 0000000000000000000000000000000000000000..58a460c883b291390ef853d2942e30e7bb321f3d GIT binary patch literal 8365 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 D>*nd1 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs b/submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs new file mode 100644 index 0000000000000000000000000000000000000000..f0ac3641d191f9d8e2cfd07b305dfe9b32b2a702 GIT binary patch literal 10308 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

J%}xpJv4k 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 31d3baff12..d552b34cfa 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2539,12 +2539,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { let inputData = Promise() - inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, messageId: message.id) + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .message(message.id)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) }) - strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId, inputData: inputData, completed: { currencyValue, receiptMessageId in + strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: inputData, completed: { currencyValue, receiptMessageId in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 5aec0a2665..ab75c05e71 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -23,6 +23,7 @@ import InviteLinksUI import UndoUI import TelegramCallsUI import WallpaperBackgroundNode +import BotPaymentsUI private final class ChatRecentActionsListOpaqueState { let entries: [ChatRecentActionsEntry] @@ -899,6 +900,30 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { case let .stickerPack(name): let packReference: StickerPackReference = .name(name) strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) + case let .invoice(slug, invoice): + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + guard let strongSelf = self else { + return + } + let _ = strongSelf + /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in + guard let strongSelf = self, let receiptMessageId = receiptMessageId else { + return false + } + + if case .info = action { + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return true + } + return false + }), in: .current)*/ + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case let .instantView(webpage, anchor): strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) case let .join(link): diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 54a506cb7d..1d957886a2 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -26,6 +26,7 @@ import ImportStickerPackUI import PeerInfoUI import Markdown import WebUI +import BotPaymentsUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -574,5 +575,28 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }) } }) + case let .invoice(slug, invoice): + dismissInput() + if let navigationController = navigationController { + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + navigationController.pushViewController(BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in + guard let strongSelf = self, let receiptMessageId = receiptMessageId else { + return false + } + + if case .info = action { + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return true + } + return false + }), in: .current)*/ + })) + } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 8526092ffa..228f745658 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -293,6 +293,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = "https://t.me/addstickers/\(set)" } } + } else if parsedUrl.host == "invoice" { + if let components = URLComponents(string: "/?" + query) { + var slug: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "slug" { + slug = value + } + } + } + } + if let slug = slug { + convertedUrl = "https://t.me/invoice/\(slug)" + } + } } else if parsedUrl.host == "setlanguage" { if let components = URLComponents(string: "/?" + query) { var lang: String? diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f2a94e1547..a61135657f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -66,6 +66,7 @@ import QrCodeUI import TranslateUI import ChatPresentationInterfaceState import CreateExternalMediaStreamScreen +import PaymentMethodUI protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -463,6 +464,7 @@ private final class PeerInfoInteraction { let requestLayout: (Bool) -> Void let openEncryptionKey: () -> Void let openSettings: (PeerInfoSettingsSection) -> Void + let openPaymentMethod: () -> Void let switchToAccount: (AccountRecordId) -> Void let logoutAccount: (AccountRecordId) -> Void let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void @@ -505,6 +507,7 @@ private final class PeerInfoInteraction { requestLayout: @escaping (Bool) -> Void, openEncryptionKey: @escaping () -> Void, openSettings: @escaping (PeerInfoSettingsSection) -> Void, + openPaymentMethod: @escaping () -> Void, switchToAccount: @escaping (AccountRecordId) -> Void, logoutAccount: @escaping (AccountRecordId) -> Void, accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, @@ -546,6 +549,7 @@ private final class PeerInfoInteraction { self.requestLayout = requestLayout self.openEncryptionKey = openEncryptionKey self.openSettings = openSettings + self.openPaymentMethod = openPaymentMethod self.switchToAccount = switchToAccount self.logoutAccount = logoutAccount self.accountContextMenu = accountContextMenu @@ -568,6 +572,7 @@ private enum SettingsSection: Int, CaseIterable { case proxy case shortcuts case advanced + case payment case extra case support } @@ -717,6 +722,10 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p interaction.openSettings(.language) })) + /*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: "Payment Method", icon: PresentationResourcesSettings.language, action: { + interaction.openPaymentMethod() + }))*/ + let stickersLabel: String if let settings = data.globalSettings { stickersLabel = settings.unreadTrendingStickerPacks > 0 ? "\(settings.unreadTrendingStickerPacks)" : "" @@ -1785,6 +1794,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate openSettings: { [weak self] section in self?.openSettings(section: section) }, + openPaymentMethod: { [weak self] in + self?.openPaymentMethod() + }, switchToAccount: { [weak self] accountId in self?.switchToAccount(id: accountId) }, @@ -6225,6 +6237,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } + fileprivate func openPaymentMethod() { + self.controller?.push(AddPaymentMethodSheetScreen(context: self.context, action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.push(PaymentCardEntryScreen(context: strongSelf.context, completion: { result in + guard let strongSelf = self else { + return + } + strongSelf.controller?.push(paymentMethodListScreen(context: strongSelf.context, items: [result])) + })) + })) + } + private func openFaq(anchor: String? = nil) { let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index ab3aabde13..ba328d1e80 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -254,7 +254,7 @@ private final class TranslateScreenComponent: CombinedComponent { } let originalTitle = originalTitle.update( component: MultilineTextComponent( - text: NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural), + text: .plain(NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), @@ -264,7 +264,7 @@ private final class TranslateScreenComponent: CombinedComponent { let originalText = originalText.update( component: MultilineTextComponent( - text: NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural), + text: .plain(NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: state.textExpanded ? 0 : 1, lineSpacing: 0.1 @@ -276,7 +276,7 @@ private final class TranslateScreenComponent: CombinedComponent { let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? "" let translationTitle = translationTitle.update( component: MultilineTextComponent( - text: NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural), + text: .plain(NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), @@ -291,7 +291,7 @@ private final class TranslateScreenComponent: CombinedComponent { if let translatedText = state.translatedText { maybeTranslationText = translationText.update( component: MultilineTextComponent( - text: NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural), + text: .plain(NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.1 diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index c48e574efc..64b594f5f7 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -77,6 +77,7 @@ public enum ParsedInternalUrl { case peerId(PeerId) case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) case stickerPack(String) + case invoice(String) case join(String) case localization(String) case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) @@ -246,6 +247,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else if pathComponents.count == 2 || pathComponents.count == 3 { if pathComponents[0] == "addstickers" { return .stickerPack(pathComponents[1]) + } else if pathComponents[0] == "invoice" { + return .invoice(pathComponents[1]) } else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" { return .join(pathComponents[1]) } else if pathComponents[0] == "setlanguage" { @@ -558,6 +561,18 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } case let .stickerPack(name): return .single(.stickerPack(name: name)) + case let .invoice(slug): + return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { invoice -> ResolvedUrl? in + guard let invoice = invoice else { + return nil + } + return .invoice(slug: slug, invoice: invoice) + } case let .join(link): return .single(.join(link)) case let .localization(identifier): diff --git a/submodules/WallpaperBackgroundNode/BUILD b/submodules/WallpaperBackgroundNode/BUILD index 1a0c4147ac..40b4495105 100644 --- a/submodules/WallpaperBackgroundNode/BUILD +++ b/submodules/WallpaperBackgroundNode/BUILD @@ -22,6 +22,9 @@ swift_library( "//submodules/Svg:Svg", "//submodules/GZip:GZip", "//submodules/AppBundle:AppBundle", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index a38a49149d..62f4fc6297 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -12,6 +12,9 @@ import FastBlur import Svg import GZip import AppBundle +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import HierarchyTrackingLayer private let motionAmount: CGFloat = 32.0 @@ -427,6 +430,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode var isLight: Bool } private static var cachedSharedPattern: (PatternKey, UIImage)? + + private var inlineAnimationNodes: [(AnimatedStickerNode, CGPoint)] = [] + private let hierarchyTrackingLayer = HierarchyTrackingLayer() + private var activateInlineAnimationTimer: SwiftSignalKit.Timer? private let _isReady = ValuePromise(false, ignoreRepeated: true) var isReady: Signal { @@ -453,7 +460,47 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode self.addSubnode(self.contentNode) self.addSubnode(self.patternImageNode) - //self.view.addSubview(self.bakedBackgroundView) + let animationList: [(String, CGPoint)] = [ + ("ptrnCAT_1162_1918", CGPoint(x: 1162 - 256, y: 1918 - 256)), + ("ptrnDOG_0440_2284", CGPoint(x: 440 - 256, y: 2284 - 256)), + ("ptrnGLOB_0438_1553", CGPoint(x: 438 - 256, y: 1553 - 256)), + ("ptrnSLON_0906_1033", CGPoint(x: 906 - 256, y: 1033 - 256)) + ] + for (animation, relativePosition) in animationList { + let animationNode = AnimatedStickerNode() + animationNode.automaticallyLoadFirstFrame = true + animationNode.autoplay = true + self.inlineAnimationNodes.append((animationNode, relativePosition)) + self.patternImageNode.addSubnode(animationNode) + animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animation), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + } + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + for (animationNode, _) in strongSelf.inlineAnimationNodes { + animationNode.visibility = true + } + strongSelf.activateInlineAnimationTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { + guard let strongSelf = self else { + return + } + + strongSelf.inlineAnimationNodes[Int.random(in: 0 ..< strongSelf.inlineAnimationNodes.count)].0.play() + }, queue: .mainQueue()) + strongSelf.activateInlineAnimationTimer?.start() + } + self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in + guard let strongSelf = self else { + return + } + for (animationNode, _) in strongSelf.inlineAnimationNodes { + animationNode.visibility = false + } + strongSelf.activateInlineAnimationTimer?.invalidate() + } } deinit { @@ -599,6 +646,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode self.patternImageNode.layer.compositingFilter = "softLightBlendMode" } } + self.patternImageNode.isHidden = false let invertPattern = intensity < 0 if invertPattern { @@ -676,7 +724,23 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode guard let strongSelf = self else { return } - if let generator = generator { + + if var generator = generator { + generator = { arguments in + let scale = arguments.scale ?? UIScreenScale + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + context.withFlippedContext { c in + if let path = getAppBundle().path(forResource: "PATTERN_static", ofType: "svg"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + if let image = drawSvgImage(data, CGSize(width: arguments.drawingSize.width * scale, height: arguments.drawingSize.height * scale), .clear, .black, false) { + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize)) + } + } + } + + return context + } + strongSelf.validPatternImage = ValidPatternImage(wallpaper: wallpaper, generate: generator) strongSelf.validPatternGeneratedImage = nil if let size = strongSelf.validLayout { @@ -795,6 +859,13 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } self.loadPatternForSizeIfNeeded(size: size, transition: transition) + + for (animationNode, relativePosition) in self.inlineAnimationNodes { + let sizeNorm = CGSize(width: 1440, height: 2960) + let animationSize = CGSize(width: 512.0 / sizeNorm.width * size.width, height: 512.0 / sizeNorm.height * size.height) + animationNode.frame = CGRect(origin: CGPoint(x: relativePosition.x / sizeNorm.width * size.width, y: relativePosition.y / sizeNorm.height * size.height), size: animationSize) + animationNode.updateLayout(size: animationNode.frame.size) + } if isFirstLayout && !self.frame.isEmpty { self.updateScale()