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 8b9a814fab..347583660d 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -120,11 +120,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 @@ -171,7 +174,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 } @@ -180,7 +183,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) } } @@ -222,13 +234,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) { @@ -238,4 +250,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 ad6af52c43..b171a138f6 100644 --- a/submodules/PremiumUI/Sources/LimitScreen.swift +++ b/submodules/PremiumUI/Sources/LimitScreen.swift @@ -163,12 +163,12 @@ private final class LimitScreenComponent: CombinedComponent { let title = title.update( component: MultilineTextComponent( - text: NSAttributedString( + text: .plain(NSAttributedString( string: strings.Premium_LimitReached, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center - ), + )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), @@ -186,7 +186,7 @@ private final class LimitScreenComponent: CombinedComponent { let text = text.update( component: MultilineTextComponent( - text: attributedText, + text: .plain(attributedText), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 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 0000000000..955000e750 Binary files /dev/null and b/submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs differ diff --git a/submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs b/submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs new file mode 100644 index 0000000000..58a460c883 Binary files /dev/null and b/submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs differ diff --git a/submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs b/submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs new file mode 100644 index 0000000000..f0ac3641d1 Binary files /dev/null and b/submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs differ diff --git a/submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs b/submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs new file mode 100644 index 0000000000..eb6397f69e Binary files /dev/null and b/submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0ed33b837c..850c173e5f 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 f62f9508d6..a612fe6fa5 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 5c7ff4bf52..2cc5b5c46b 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()