Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-05-08 19:35:58 +04:00
commit 6b7ed2e6ac
52 changed files with 3897 additions and 166 deletions

View File

@ -252,13 +252,11 @@ public enum ResolvedUrl {
case share(url: String?, text: String?, to: String?) case share(url: String?, text: String?, to: String?)
case wallpaper(WallpaperUrlParameter) case wallpaper(WallpaperUrlParameter)
case theme(String) case theme(String)
#if ENABLE_WALLET
case wallet(address: String, amount: Int64?, comment: String?)
#endif
case settings(ResolvedUrlSettingsSection) case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?) case joinVoiceChat(PeerId, String?)
case importStickers case importStickers
case startAttach(peerId: PeerId, payload: String?) case startAttach(peerId: PeerId, payload: String?)
case invoice(slug: String, invoice: TelegramMediaInvoice)
} }
public enum NavigateToChatKeepStack { public enum NavigateToChatKeepStack {

View File

@ -24,7 +24,7 @@ public final class BotCheckoutController: ViewController {
self.validatedFormInfo = validatedFormInfo self.validatedFormInfo = validatedFormInfo
} }
public static func fetch(context: AccountContext, messageId: EngineMessage.Id) -> Signal<InputData, FetchError> { public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let themeParams: [String: Any] = [ let themeParams: [String: Any] = [
"bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb), "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) "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 |> mapError { _ -> FetchError in
return .generic return .generic
} }
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in |> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
if let current = paymentForm.savedInfo { 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 |> mapError { _ -> FetchError in
return .generic return .generic
} }
@ -77,7 +77,7 @@ public final class BotCheckoutController: ViewController {
private let context: AccountContext private let context: AccountContext
private let invoice: TelegramMediaInvoice private let invoice: TelegramMediaInvoice
private let messageId: EngineMessage.Id private let source: BotPaymentInvoiceSource
private let completed: (String, EngineMessage.Id?) -> Void private let completed: (String, EngineMessage.Id?) -> Void
private var presentationData: PresentationData private var presentationData: PresentationData
@ -86,10 +86,10 @@ public final class BotCheckoutController: ViewController {
private let inputData: Promise<BotCheckoutController.InputData?> private let inputData: Promise<BotCheckoutController.InputData?>
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: EngineMessage.Id, inputData: Promise<BotCheckoutController.InputData?>, completed: @escaping (String, EngineMessage.Id?) -> Void) { public init(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Promise<BotCheckoutController.InputData?>, completed: @escaping (String, EngineMessage.Id?) -> Void) {
self.context = context self.context = context
self.invoice = invoice self.invoice = invoice
self.messageId = messageId self.source = source
self.inputData = inputData self.inputData = inputData
self.completed = completed self.completed = completed
@ -113,7 +113,7 @@ public final class BotCheckoutController: ViewController {
} }
override public func loadDisplayNode() { 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) self?.present(c, in: .window(.root), with: a)
}, dismissAnimated: { [weak self] in }, dismissAnimated: { [weak self] in
self?.dismiss() self?.dismiss()

View File

@ -493,7 +493,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private weak var controller: BotCheckoutController? private weak var controller: BotCheckoutController?
private let navigationBar: NavigationBar private let navigationBar: NavigationBar
private let context: AccountContext private let context: AccountContext
private let messageId: EngineMessage.Id private let source: BotPaymentInvoiceSource
private let present: (ViewController, Any?) -> Void private let present: (ViewController, Any?) -> Void
private let dismissAnimated: () -> Void private let dismissAnimated: () -> Void
private let completed: (String, EngineMessage.Id?) -> Void private let completed: (String, EngineMessage.Id?) -> Void
@ -527,11 +527,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
private var passwordTip: String? private var passwordTip: String?
private var passwordTipDisposable: Disposable? private var passwordTipDisposable: Disposable?
init(controller: BotCheckoutController?, navigationBar: NavigationBar, context: AccountContext, invoice: TelegramMediaInvoice, messageId: EngineMessage.Id, inputData: Promise<BotCheckoutController.InputData?>, 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<BotCheckoutController.InputData?>, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void, completed: @escaping (String, EngineMessage.Id?) -> Void) {
self.controller = controller self.controller = controller
self.navigationBar = navigationBar self.navigationBar = navigationBar
self.context = context self.context = context
self.messageId = messageId self.source = source
self.present = present self.present = present
self.dismissAnimated = dismissAnimated self.dismissAnimated = dismissAnimated
self.completed = completed self.completed = completed
@ -603,7 +603,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
openInfoImpl = { [weak self] focus in openInfoImpl = { [weak self] focus in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.controller?.view.endEditing(true) 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 { if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue {
strongSelf.currentFormInfo = formInfo strongSelf.currentFormInfo = formInfo
strongSelf.currentValidatedFormInfo = validatedInfo strongSelf.currentValidatedFormInfo = validatedInfo
@ -1125,7 +1125,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
countryCode = paramsCountryCode countryCode = paramsCountryCode
} }
let botPeerId = self.messageId.peerId let botPeerId = paymentForm.paymentBotId
let _ = (context.engine.data.get( let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: botPeerId) 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 totalAmount = currentTotalPrice(paymentForm: paymentForm, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount)
let currencyValue = formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency) 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 { if let strongSelf = self {
strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.isUserInteractionEnabled = false
strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.inProgressDimNode.alpha = 0.0

View File

@ -30,7 +30,7 @@ final class BotCheckoutInfoController: ViewController {
private let context: AccountContext private let context: AccountContext
private let invoice: BotPaymentInvoice private let invoice: BotPaymentInvoice
private let messageId: EngineMessage.Id private let source: BotPaymentInvoiceSource
private let initialFormInfo: BotPaymentRequestedInfo private let initialFormInfo: BotPaymentRequestedInfo
private let focus: BotCheckoutInfoControllerFocus private let focus: BotCheckoutInfoControllerFocus
@ -46,14 +46,14 @@ final class BotCheckoutInfoController: ViewController {
public init( public init(
context: AccountContext, context: AccountContext,
invoice: BotPaymentInvoice, invoice: BotPaymentInvoice,
messageId: EngineMessage.Id, source: BotPaymentInvoiceSource,
initialFormInfo: BotPaymentRequestedInfo, initialFormInfo: BotPaymentRequestedInfo,
focus: BotCheckoutInfoControllerFocus, focus: BotCheckoutInfoControllerFocus,
formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void
) { ) {
self.context = context self.context = context
self.invoice = invoice self.invoice = invoice
self.messageId = messageId self.source = source
self.initialFormInfo = initialFormInfo self.initialFormInfo = initialFormInfo
self.focus = focus self.focus = focus
self.formInfoUpdated = formInfoUpdated self.formInfoUpdated = formInfoUpdated
@ -80,7 +80,7 @@ final class BotCheckoutInfoController: ViewController {
} }
override public func loadDisplayNode() { 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) self?.presentingViewController?.dismiss(animated: false, completion: nil)
}, openCountrySelection: { [weak self] in }, openCountrySelection: { [weak self] in
if let strongSelf = self { if let strongSelf = self {

View File

@ -96,7 +96,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
private let context: AccountContext private let context: AccountContext
private weak var navigationBar: NavigationBar? private weak var navigationBar: NavigationBar?
private let invoice: BotPaymentInvoice private let invoice: BotPaymentInvoice
private let messageId: EngineMessage.Id private let source: BotPaymentInvoiceSource
private var focus: BotCheckoutInfoControllerFocus? private var focus: BotCheckoutInfoControllerFocus?
private let dismiss: () -> Void private let dismiss: () -> Void
@ -130,7 +130,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
context: AccountContext, context: AccountContext,
navigationBar: NavigationBar?, navigationBar: NavigationBar?,
invoice: BotPaymentInvoice, invoice: BotPaymentInvoice,
messageId: EngineMessage.Id, source: BotPaymentInvoiceSource,
formInfo: BotPaymentRequestedInfo, formInfo: BotPaymentRequestedInfo,
focus: BotCheckoutInfoControllerFocus, focus: BotCheckoutInfoControllerFocus,
theme: PresentationTheme, theme: PresentationTheme,
@ -144,7 +144,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
self.context = context self.context = context
self.navigationBar = navigationBar self.navigationBar = navigationBar
self.invoice = invoice self.invoice = invoice
self.messageId = messageId self.source = source
self.formInfo = formInfo self.formInfo = formInfo
self.focus = focus self.focus = focus
self.dismiss = dismiss self.dismiss = dismiss
@ -367,7 +367,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
func verify() { func verify() {
self.isVerifying = true self.isVerifying = true
let formInfo = self.collectFormInfo() 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 { if let strongSelf = self {
strongSelf.formInfoUpdated(formInfo, result) strongSelf.formInfoUpdated(formInfo, result)
} }

View File

@ -104,8 +104,9 @@ public final class _ConcreteChildComponent<ComponentType: Component>: _AnyChildC
let context = view.context(component: component) let context = view.context(component: component)
EnvironmentBuilder._environment = context.erasedEnvironment EnvironmentBuilder._environment = context.erasedEnvironment
let _ = environment() let environmentResult = environment()
EnvironmentBuilder._environment = nil EnvironmentBuilder._environment = nil
context.erasedEnvironment = environmentResult
return updateChildAnyComponent( return updateChildAnyComponent(
id: self.id, id: self.id,
@ -288,9 +289,11 @@ public final class _EnvironmentChildComponent<EnvironmentType>: _AnyChildCompone
transition = .immediate transition = .immediate
} }
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment let viewContext = view.context(component: component)
let _ = environment() EnvironmentBuilder._environment = viewContext.erasedEnvironment
let environmentResult = environment()
EnvironmentBuilder._environment = nil EnvironmentBuilder._environment = nil
viewContext.erasedEnvironment = environmentResult
return updateChildAnyComponent( return updateChildAnyComponent(
id: self.id, id: self.id,
@ -342,9 +345,11 @@ public final class _EnvironmentChildComponentFromMap<EnvironmentType>: _AnyChild
transition = .immediate transition = .immediate
} }
EnvironmentBuilder._environment = view.context(component: component).erasedEnvironment let viewContext = view.context(component: component)
let _ = environment() EnvironmentBuilder._environment = viewContext.erasedEnvironment
let environmentResult = environment()
EnvironmentBuilder._environment = nil EnvironmentBuilder._environment = nil
viewContext.erasedEnvironment = environmentResult
return updateChildAnyComponent( return updateChildAnyComponent(
id: self.id, id: self.id,

View File

@ -26,7 +26,11 @@ class AnyComponentContext<EnvironmentType>: _TypeErasedComponentContext {
preconditionFailure() preconditionFailure()
} }
var erasedEnvironment: _Environment { var erasedEnvironment: _Environment {
get {
return self.environment return self.environment
} set(value) {
self.environment = value as! Environment<EnvironmentType>
}
} }
let layoutResult: ComponentLayoutResult let layoutResult: ComponentLayoutResult

View File

@ -23,13 +23,13 @@ public final class Button: Component {
private init( private init(
content: AnyComponent<Empty>, content: AnyComponent<Empty>,
minSize: CGSize?, minSize: CGSize? = nil,
tag: AnyObject? = nil, tag: AnyObject? = nil,
automaticHighlight: Bool = true, automaticHighlight: Bool = true,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.content = content self.content = content
self.minSize = nil self.minSize = minSize
self.tag = tag self.tag = tag
self.automaticHighlight = automaticHighlight self.automaticHighlight = automaticHighlight
self.action = action self.action = action

View File

@ -17,7 +17,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? {
return nil return nil
} }
public final class ComponentHostView<EnvironmentType: Equatable>: UIView { public final class ComponentHostView<EnvironmentType>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>? private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize? private var currentContainerSize: CGSize?
private var currentSize: CGSize? private var currentSize: CGSize?
@ -43,9 +43,7 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
self.isUpdating = true self.isUpdating = true
precondition(containerSize.width.isFinite) precondition(containerSize.width.isFinite)
precondition(containerSize.width < .greatestFiniteMagnitude)
precondition(containerSize.height.isFinite) precondition(containerSize.height.isFinite)
precondition(containerSize.height < .greatestFiniteMagnitude)
let componentView: UIView let componentView: UIView
if let current = self.componentView { if let current = self.componentView {
@ -62,8 +60,9 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
if updateEnvironment { if updateEnvironment {
EnvironmentBuilder._environment = context.erasedEnvironment EnvironmentBuilder._environment = context.erasedEnvironment
let _ = maybeEnvironment() let environmentResult = maybeEnvironment()
EnvironmentBuilder._environment = nil EnvironmentBuilder._environment = nil
context.erasedEnvironment = environmentResult
} }
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()

View File

@ -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",
],
)

View File

@ -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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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",
],
)

View File

@ -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<Empty>
public let content: AnyComponentWithIdentity<Empty>
public init(prefix: AnyComponentWithIdentity<Empty>, content: AnyComponentWithIdentity<Empty>) {
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<Empty>] = [:]
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<Empty>, 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<Empty>
var itemTransition = transition
if let current = self.itemViews[item.prefix.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
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<Empty>
var itemTransition = transition
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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",
],
)

View File

@ -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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -12,9 +12,7 @@ swift_library(
deps = [ deps = [
"//submodules/Display:Display", "//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow", "//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Markdown:Markdown",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -2,21 +2,27 @@ import Foundation
import UIKit import UIKit
import ComponentFlow import ComponentFlow
import Display import Display
import Markdown
public final class MultilineTextComponent: Component { 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 horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment public let verticalAlignment: TextVerticalAlignment
public var truncationType: CTLineTruncationType public let truncationType: CTLineTruncationType
public var maximumNumberOfLines: Int public let maximumNumberOfLines: Int
public var lineSpacing: CGFloat public let lineSpacing: CGFloat
public var cutout: TextNodeCutout? public let cutout: TextNodeCutout?
public var insets: UIEdgeInsets public let insets: UIEdgeInsets
public var textShadowColor: UIColor? public let textShadowColor: UIColor?
public var textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public init( public init(
text: NSAttributedString, text: TextContent,
horizontalAlignment: NSTextAlignment = .natural, horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top, verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end, truncationType: CTLineTruncationType = .end,
@ -40,7 +46,7 @@ public final class MultilineTextComponent: Component {
} }
public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool { public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool {
if !lhs.text.isEqual(to: rhs.text) { if lhs.text != rhs.text {
return false return false
} }
if lhs.horizontalAlignment != rhs.horizontalAlignment { if lhs.horizontalAlignment != rhs.horizontalAlignment {
@ -89,9 +95,17 @@ public final class MultilineTextComponent: Component {
public final class View: TextView { public final class View: TextView {
public func update(component: MultilineTextComponent, availableSize: CGSize) -> CGSize { 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 makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments( let (layout, apply) = makeLayout(TextNodeLayoutArguments(
attributedString: component.text, attributedString: attributedString,
backgroundColor: nil, backgroundColor: nil,
maximumNumberOfLines: component.maximumNumberOfLines, maximumNumberOfLines: component.maximumNumberOfLines,
truncationType: component.truncationType, truncationType: component.truncationType,

View File

@ -120,11 +120,14 @@ open class ViewControllerComponentContainer: ViewController {
} }
} }
public final class AnimateInTransition {
}
public final class Node: ViewControllerTracingNode { public final class Node: ViewControllerTracingNode {
private var presentationData: PresentationData private var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer? private weak var controller: ViewControllerComponentContainer?
private let component: AnyComponent<ViewControllerComponentContainer.Environment> private var component: AnyComponent<ViewControllerComponentContainer.Environment>
private let theme: PresentationTheme? private let theme: PresentationTheme?
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment> public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
@ -171,7 +174,7 @@ open class ViewControllerComponentContainer: ViewController {
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) 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 { if self.currentIsVisible == isVisible {
return return
} }
@ -180,7 +183,16 @@ open class ViewControllerComponentContainer: ViewController {
guard let currentLayout = self.currentLayout else { guard let currentLayout = self.currentLayout else {
return 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<ViewControllerComponentContainer.Environment>, 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) { override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.node.updateIsVisible(isVisible: true) self.node.updateIsVisible(isVisible: true, animated: true)
} }
override open func viewDidDisappear(_ animated: Bool) { override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
self.node.updateIsVisible(isVisible: false) self.node.updateIsVisible(isVisible: false, animated: false)
} }
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { 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)) self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
} }
public func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: Transition) {
self.node.updateComponent(component: component, transition: transition)
}
} }

View File

@ -12,15 +12,15 @@ import AccountContext
import Markdown import Markdown
import TextFormat import TextFormat
class InviteLinkHeaderItem: ListViewItem, ItemListItem { public class InviteLinkHeaderItem: ListViewItem, ItemListItem {
let context: AccountContext public let context: AccountContext
let theme: PresentationTheme public let theme: PresentationTheme
let text: String public let text: String
let animationName: String public let animationName: String
let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let linkAction: ((ItemListTextItemLinkAction) -> Void)? 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.context = context
self.theme = theme self.theme = theme
self.text = text self.text = text
@ -29,7 +29,7 @@ class InviteLinkHeaderItem: ListViewItem, ItemListItem {
self.linkAction = linkAction self.linkAction = linkAction
} }
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async { async {
let node = InviteLinkHeaderItemNode() let node = InviteLinkHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) 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 { Queue.mainQueue().async {
guard let nodeValue = node() as? InviteLinkHeaderItemNode else { guard let nodeValue = node() as? InviteLinkHeaderItemNode else {
assertionFailure() assertionFailure()

View File

@ -31,6 +31,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
let iconSize: CGSize? let iconSize: CGSize?
let iconPlacement: IconPlacement let iconPlacement: IconPlacement
let title: String let title: String
let subtitle: String?
let style: ItemListCheckboxItemStyle let style: ItemListCheckboxItemStyle
let color: ItemListCheckboxItemColor let color: ItemListCheckboxItemColor
let textColor: TextColor let textColor: TextColor
@ -40,12 +41,13 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
let action: () -> Void let action: () -> Void
let deleteAction: (() -> 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.presentationData = presentationData
self.icon = icon self.icon = icon
self.iconSize = iconSize self.iconSize = iconSize
self.iconPlacement = iconPlacement self.iconPlacement = iconPlacement
self.title = title self.title = title
self.subtitle = subtitle
self.style = style self.style = style
self.color = color self.color = color
self.textColor = textColor self.textColor = textColor
@ -111,6 +113,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
private let imageNode: ASImageNode private let imageNode: ASImageNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let titleNode: TextNode private let titleNode: TextNode
private let subtitleNode: TextNode
private var item: ItemListCheckboxItem? private var item: ItemListCheckboxItem?
@ -149,6 +152,11 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
self.titleNode.contentMode = .left self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale 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 = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
@ -161,6 +169,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
self.contentContainerNode.addSubnode(self.imageNode) self.contentContainerNode.addSubnode(self.imageNode)
self.contentContainerNode.addSubnode(self.iconNode) self.contentContainerNode.addSubnode(self.iconNode)
self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.subtitleNode)
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in 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) { public func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.item let currentItem = self.item
@ -181,7 +191,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
case .left: case .left:
leftInset += 62.0 leftInset += 62.0
case .right: case .right:
leftInset += 16.0 leftInset += 0.0
} }
let iconInset: CGFloat = 62.0 let iconInset: CGFloat = 62.0
@ -195,8 +205,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
} }
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) 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 titleColor: UIColor
let subtitleColor: UIColor = item.presentationData.theme.list.itemSecondaryTextColor
switch item.textColor { switch item.textColor {
case .primary: case .primary:
titleColor = item.presentationData.theme.list.itemPrimaryTextColor 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 (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 separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors, params) 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) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -257,6 +274,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
} }
let _ = titleApply() let _ = titleApply()
let _ = subtitleApply()
if let image = strongSelf.iconNode.image { if let image = strongSelf.iconNode.image {
switch item.style { 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.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.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 { if let icon = item.icon {
let iconSize = item.iconSize ?? icon.size let iconSize = item.iconSize ?? icon.size

View File

@ -4,7 +4,7 @@ import UIKit
private let controlStartCharactersSet = CharacterSet(charactersIn: "[*") private let controlStartCharactersSet = CharacterSet(charactersIn: "[*")
private let controlCharactersSet = CharacterSet(charactersIn: "[]()*_-\\") private let controlCharactersSet = CharacterSet(charactersIn: "[]()*_-\\")
public final class MarkdownAttributeSet { public final class MarkdownAttributeSet: Equatable {
public let font: UIFont public let font: UIFont
public let textColor: UIColor public let textColor: UIColor
public let additionalAttributes: [String: Any] public let additionalAttributes: [String: Any]
@ -14,9 +14,19 @@ public final class MarkdownAttributeSet {
self.textColor = textColor self.textColor = textColor
self.additionalAttributes = additionalAttributes 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 body: MarkdownAttributeSet
public let bold: MarkdownAttributeSet public let bold: MarkdownAttributeSet
public let link: MarkdownAttributeSet public let link: MarkdownAttributeSet
@ -28,6 +38,19 @@ public final class MarkdownAttributes {
self.bold = bold self.bold = bold
self.linkAttribute = linkAttribute 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 { public func escapedPlaintextForMarkdown(_ string: String) -> String {

View File

@ -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",
],
)

View File

@ -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<ChildEnvironmentType: Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment)
public let content: AnyComponent<ChildEnvironmentType>
public let backgroundColor: UIColor
public let animateOut: ActionSlot<Action<()>>
public init(content: AnyComponent<ChildEnvironmentType>, backgroundColor: UIColor, animateOut: ActionSlot<Action<()>>) {
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<ChildEnvironmentType>
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<ChildEnvironmentType>()
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<CGPoint>) {
}
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<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, 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<EnvironmentType>, 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<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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
}
}

View File

@ -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<ChildEnvironment: Equatable>: 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<ChildEnvironment>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, 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<ChildEnvironment>, 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<CardEntryModel, String>, String) -> Void
init(context: AccountContext, model: CardEntryModel, updateModelKey: @escaping (WritableKeyPath<CardEntryModel, String>, 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<CardEntryModel, String>, String) -> Void
init(context: AccountContext, model: CardEntryModel, updateModelKey: @escaping(WritableKeyPath<CardEntryModel, String>, 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<EnvironmentType>.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<EnvironmentType>(
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<CardEntryModel, String>, 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<CardEntryModel, String>, 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()
}
}

View File

@ -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<PresentationData, NoError>)? = 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
}

View File

@ -216,7 +216,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let text = text.update( let text = text.update(
component: MultilineTextComponent( 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, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.1 lineSpacing: 0.1
@ -228,7 +228,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let bottomText = Condition(mode == .create) { let bottomText = Condition(mode == .create) {
bottomText.update( bottomText.update(
component: MultilineTextComponent( 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, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.1 lineSpacing: 0.1
@ -290,7 +290,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
if let credentials = context.state.credentials { if let credentials = context.state.credentials {
let credentialsURLTitle = credentialsURLTitle.update( let credentialsURLTitle = credentialsURLTitle.update(
component: MultilineTextComponent( 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, horizontalAlignment: .left,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
@ -300,7 +300,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsKeyTitle = credentialsKeyTitle.update( let credentialsKeyTitle = credentialsKeyTitle.update(
component: MultilineTextComponent( 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, horizontalAlignment: .left,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
@ -310,7 +310,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsURLText = credentialsURLText.update( let credentialsURLText = credentialsURLText.update(
component: MultilineTextComponent( 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, horizontalAlignment: .left,
truncationType: .middle, truncationType: .middle,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
@ -321,7 +321,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsKeyText = credentialsKeyText.update( let credentialsKeyText = credentialsKeyText.update(
component: MultilineTextComponent( 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, horizontalAlignment: .left,
truncationType: .middle, truncationType: .middle,
maximumNumberOfLines: 1 maximumNumberOfLines: 1

View File

@ -163,12 +163,12 @@ private final class LimitScreenComponent: CombinedComponent {
let title = title.update( let title = title.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: NSAttributedString( text: .plain(NSAttributedString(
string: strings.Premium_LimitReached, string: strings.Premium_LimitReached,
font: Font.semibold(17.0), font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor, textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center paragraphAlignment: .center
), )),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
@ -186,7 +186,7 @@ private final class LimitScreenComponent: CombinedComponent {
let text = text.update( let text = text.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: attributedText, text: .plain(attributedText),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.2 lineSpacing: 0.2

View File

@ -99,6 +99,7 @@ swift_library(
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/FetchManagerImpl:FetchManagerImpl", "//submodules/FetchManagerImpl:FetchManagerImpl",
"//submodules/ListMessageItem:ListMessageItem", "//submodules/ListMessageItem:ListMessageItem",
"//submodules/PaymentMethodUI:PaymentMethodUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -2,6 +2,7 @@
#import <Stripe/STPAddress.h> #import <Stripe/STPAddress.h>
#import <Stripe/STPPaymentCardTextField.h> #import <Stripe/STPPaymentCardTextField.h>
#import <Stripe/STPFormTextField.h>
#import <Stripe/STPAPIClient.h> #import <Stripe/STPAPIClient.h>
#import <Stripe/STPAPIClient+ApplePay.h> #import <Stripe/STPAPIClient+ApplePay.h>
#import <Stripe/STPAPIResponseDecodable.h> #import <Stripe/STPAPIResponseDecodable.h>

View File

@ -277,6 +277,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) }
dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) }
dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($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[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) }
dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) }
dict[860303448] = { return Api.InputMedia.parse_inputMediaDocument($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[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) }
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($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[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
@ -1280,6 +1283,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.InputGroupCall: case let _1 as Api.InputGroupCall:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.InputInvoice:
_1.serialize(buffer, boxed)
case let _1 as Api.InputMedia: case let _1 as Api.InputMedia:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.InputMessage: case let _1 as Api.InputMessage:
@ -1740,6 +1745,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.BankCardData: case let _1 as Api.payments.BankCardData:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.ExportedInvoice:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentForm: case let _1 as Api.payments.PaymentForm:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentReceipt: case let _1 as Api.payments.PaymentReceipt:

View File

@ -647,18 +647,57 @@ public extension Api.payments {
} }
} }
public extension Api.payments { public extension Api.payments {
enum PaymentForm: TypeConstructorDescription { enum ExportedInvoice: 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]) case exportedInvoice(url: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { 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) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(formId, buffer: buffer, boxed: false) serializeInt64(formId, buffer: buffer, boxed: false)
serializeInt64(botId, 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) invoice.serialize(buffer, true)
serializeInt64(providerId, buffer: buffer, boxed: false) serializeInt64(providerId, buffer: buffer, boxed: false)
serializeString(url, buffer: buffer, boxed: false) serializeString(url, buffer: buffer, boxed: false)
@ -677,8 +716,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { 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 .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)), ("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))]) 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() _2 = reader.readInt64()
var _3: Int64? var _3: Int64?
_3 = reader.readInt64() _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() { 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? var _8: Int64?
_5 = reader.readInt64() _8 = reader.readInt64()
var _6: String? var _9: String?
_6 = parseString(reader) _9 = parseString(reader)
var _7: String? var _10: String?
if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } if Int(_1!) & Int(1 << 4) != 0 {_10 = parseString(reader) }
var _8: Api.DataJSON? var _11: Api.DataJSON?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { 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() { 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() { 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() { 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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
let _c4 = _4 != nil let _c4 = _4 != nil
let _c5 = _5 != nil let _c5 = _5 != nil
let _c6 = _6 != nil let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil let _c8 = _8 != nil
let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil let _c9 = _9 != nil
let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil
let _c11 = _11 != nil let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { let _c12 = (Int(_1!) & Int(1 << 0) == 0) || _12 != nil
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 _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 { else {
return nil return nil

View File

@ -6192,6 +6192,21 @@ public extension Api.functions.payments {
}) })
} }
} }
public extension Api.functions.payments {
static func exportInvoice(invoiceMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.ExportedInvoice>) {
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 { public extension Api.functions.payments {
static func getBankCardData(number: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.BankCardData>) { static func getBankCardData(number: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.BankCardData>) {
let buffer = Buffer() let buffer = Buffer()
@ -6208,14 +6223,13 @@ public extension Api.functions.payments {
} }
} }
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<Api.payments.PaymentForm>) { static func getPaymentForm(flags: Int32, invoice: Api.InputInvoice, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentForm>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-1976353651) buffer.appendInt32(924093883)
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true) invoice.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {themeParams!.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) let reader = BufferReader(buffer)
var result: Api.payments.PaymentForm? var result: Api.payments.PaymentForm?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
@ -6257,18 +6271,17 @@ public extension Api.functions.payments {
} }
} }
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<Api.payments.PaymentResult>) { static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentResult>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(818134173) buffer.appendInt32(755192367)
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(formId, buffer: buffer, boxed: false) serializeInt64(formId, buffer: buffer, boxed: false)
peer.serialize(buffer, true) invoice.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(requestedInfoId!, buffer: buffer, boxed: false)} 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)} if Int(flags) & Int(1 << 1) != 0 {serializeString(shippingOptionId!, buffer: buffer, boxed: false)}
credentials.serialize(buffer, true) credentials.serialize(buffer, true)
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} 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) let reader = BufferReader(buffer)
var result: Api.payments.PaymentResult? var result: Api.payments.PaymentResult?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
@ -6279,14 +6292,13 @@ public extension Api.functions.payments {
} }
} }
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<Api.payments.ValidatedRequestedInfo>) { static func validateRequestedInfo(flags: Int32, invoice: Api.InputInvoice, info: Api.PaymentRequestedInfo) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.ValidatedRequestedInfo>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-619695760) buffer.appendInt32(-1228345045)
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true) invoice.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
info.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) let reader = BufferReader(buffer)
var result: Api.payments.ValidatedRequestedInfo? var result: Api.payments.ValidatedRequestedInfo?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -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 { public extension Api {
enum InputMedia: TypeConstructorDescription { enum InputMedia: TypeConstructorDescription {
case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String)

View File

@ -286,7 +286,7 @@ final class MediaStreamVideoComponent: Component {
let noSignalSize = noSignalView.update( let noSignalSize = noSignalView.update(
transition: transition, transition: transition,
component: AnyComponent(MultilineTextComponent( 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, horizontalAlignment: .center,
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)), )),

View File

@ -4,6 +4,10 @@ import MtProtoKit
import SwiftSignalKit import SwiftSignalKit
import TelegramApi import TelegramApi
public enum BotPaymentInvoiceSource {
case message(MessageId)
case slug(String)
}
public struct BotPaymentInvoiceFields: OptionSet { public struct BotPaymentInvoiceFields: OptionSet {
public var rawValue: Int32 public var rawValue: Int32
@ -173,15 +177,70 @@ extension BotPaymentRequestedInfo {
} }
} }
func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, messageId: MessageId, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> { func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> {
return postbox.transaction { transaction -> Api.InputPeer? in return postbox.transaction { transaction -> Api.InputInvoice? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) 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) |> castError(BotPaymentFormRequestError.self)
|> mapToSignal { inputPeer -> Signal<BotPaymentForm, BotPaymentFormRequestError> in |> mapToSignal { invoice -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> in
guard let inputPeer = inputPeer else { guard let invoice = invoice else {
return .fail(.generic) return .fail(.generic)
} }
let flags: Int32 = 0
return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: nil))
|> `catch` { _ -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
return .fail(.generic)
}
|> mapToSignal { result -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> 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<BotPaymentForm, BotPaymentFormRequestError> {
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<BotPaymentForm, BotPaymentFormRequestError> in
guard let invoice = invoice else {
return .fail(.generic)
}
var flags: Int32 = 0 var flags: Int32 = 0
var serializedThemeParams: Api.DataJSON? var serializedThemeParams: Api.DataJSON?
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { 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 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<Api.payments.PaymentForm, BotPaymentFormRequestError> in |> `catch` { _ -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
return .fail(.generic) return .fail(.generic)
} }
|> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> in |> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> in
return postbox.transaction { transaction -> BotPaymentForm in return postbox.transaction { transaction -> BotPaymentForm in
switch result { 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] = [] var peers: [Peer] = []
for user in apiUsers { for user in apiUsers {
let parsed = TelegramUser(user: user) let parsed = TelegramUser(user: user)
@ -268,13 +331,21 @@ extension BotPaymentShippingOption {
} }
} }
func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> { func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in return account.postbox.transaction { transaction -> Api.InputInvoice? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) 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) |> castError(ValidateBotPaymentFormError.self)
|> mapToSignal { inputPeer -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> in |> mapToSignal { invoice -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> in
guard let inputPeer = inputPeer else { guard let invoice = invoice else {
return .fail(.generic) return .fail(.generic)
} }
@ -297,7 +368,7 @@ func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, messageI
infoFlags |= (1 << 3) infoFlags |= (1 << 3)
apiShippingAddress = .postAddress(streetLine1: address.streetLine1, streetLine2: address.streetLine2, city: address.city, state: address.state, countryIso2: address.countryIso2, postCode: address.postCode) 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 |> mapError { error -> ValidateBotPaymentFormError in
if error.errorDescription == "SHIPPING_NOT_AVAILABLE" { if error.errorDescription == "SHIPPING_NOT_AVAILABLE" {
return .shippingNotAvailable return .shippingNotAvailable
@ -346,13 +417,21 @@ public enum SendBotPaymentResult {
case externalVerificationRequired(url: String) case externalVerificationRequired(url: String)
} }
func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> { func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in return account.postbox.transaction { transaction -> Api.InputInvoice? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) 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) |> castError(SendBotPaymentFormError.self)
|> mapToSignal { inputPeer -> Signal<SendBotPaymentResult, SendBotPaymentFormError> in |> mapToSignal { invoice -> Signal<SendBotPaymentResult, SendBotPaymentFormError> in
guard let inputPeer = inputPeer else { guard let invoice = invoice else {
return .fail(.generic) return .fail(.generic)
} }
@ -379,7 +458,8 @@ func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId
if tipAmount != nil { if tipAmount != nil {
flags |= (1 << 2) 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 |> map { result -> SendBotPaymentResult in
switch result { switch result {
case let .paymentResult(updates): case let .paymentResult(updates):
@ -392,11 +472,16 @@ func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId
if case .paymentSent = action.action { if case .paymentSent = action.action {
for attribute in message.attributes { for attribute in message.attributes {
if let reply = attribute as? ReplyMessageAttribute { if let reply = attribute as? ReplyMessageAttribute {
switch source {
case let .message(messageId):
if reply.messageId == messageId { if reply.messageId == messageId {
if case let .Id(id) = message.id { if case let .Id(id) = message.id {
receiptMessageId = id receiptMessageId = id
} }
} }
case .slug:
break
}
} }
} }
} }

View File

@ -13,16 +13,20 @@ public extension TelegramEngine {
return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber) return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber)
} }
public func fetchBotPaymentForm(messageId: MessageId, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> { public func fetchBotPaymentInvoice(source: BotPaymentInvoiceSource) -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> {
return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, messageId: messageId, themeParams: themeParams) return _internal_fetchBotPaymentInvoice(postbox: self.account.postbox, network: self.account.network, source: source)
} }
public func validateBotPaymentForm(saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> { public func fetchBotPaymentForm(source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> {
return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, messageId: messageId, formInfo: formInfo) return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, source: source, themeParams: themeParams)
} }
public func sendBotPaymentForm(messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> { public func validateBotPaymentForm(saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> {
return _internal_sendBotPaymentForm(account: self.account, messageId: messageId, formId: formId, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, source: source, formInfo: formInfo)
}
public func sendBotPaymentForm(source: BotPaymentInvoiceSource, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return _internal_sendBotPaymentForm(account: self.account, formId: formId, source: source, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials)
} }
public func requestBotPaymentReceipt(messageId: MessageId) -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> { public func requestBotPaymentReceipt(messageId: MessageId) -> Signal<BotPaymentReceipt, RequestBotPaymentReceiptError> {

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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)) strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else { } else {
let inputData = Promise<BotCheckoutController.InputData?>() let inputData = Promise<BotCheckoutController.InputData?>()
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) |> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in |> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil) 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 { guard let strongSelf = self else {
return return
} }

View File

@ -23,6 +23,7 @@ import InviteLinksUI
import UndoUI import UndoUI
import TelegramCallsUI import TelegramCallsUI
import WallpaperBackgroundNode import WallpaperBackgroundNode
import BotPaymentsUI
private final class ChatRecentActionsListOpaqueState { private final class ChatRecentActionsListOpaqueState {
let entries: [ChatRecentActionsEntry] let entries: [ChatRecentActionsEntry]
@ -899,6 +900,30 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
case let .stickerPack(name): case let .stickerPack(name):
let packReference: StickerPackReference = .name(name) let packReference: StickerPackReference = .name(name)
strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil)
case let .invoice(slug, invoice):
let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> 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): case let .instantView(webpage, anchor):
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: .channel, anchor: anchor))
case let .join(link): case let .join(link):

View File

@ -26,6 +26,7 @@ import ImportStickerPackUI
import PeerInfoUI import PeerInfoUI
import Markdown import Markdown
import WebUI import WebUI
import BotPaymentsUI
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation { 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<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: context, source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> 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)*/
}))
}
} }
} }

View File

@ -293,6 +293,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
convertedUrl = "https://t.me/addstickers/\(set)" 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" { } else if parsedUrl.host == "setlanguage" {
if let components = URLComponents(string: "/?" + query) { if let components = URLComponents(string: "/?" + query) {
var lang: String? var lang: String?

View File

@ -66,6 +66,7 @@ import QrCodeUI
import TranslateUI import TranslateUI
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import CreateExternalMediaStreamScreen import CreateExternalMediaStreamScreen
import PaymentMethodUI
protocol PeerInfoScreenItem: AnyObject { protocol PeerInfoScreenItem: AnyObject {
var id: AnyHashable { get } var id: AnyHashable { get }
@ -463,6 +464,7 @@ private final class PeerInfoInteraction {
let requestLayout: (Bool) -> Void let requestLayout: (Bool) -> Void
let openEncryptionKey: () -> Void let openEncryptionKey: () -> Void
let openSettings: (PeerInfoSettingsSection) -> Void let openSettings: (PeerInfoSettingsSection) -> Void
let openPaymentMethod: () -> Void
let switchToAccount: (AccountRecordId) -> Void let switchToAccount: (AccountRecordId) -> Void
let logoutAccount: (AccountRecordId) -> Void let logoutAccount: (AccountRecordId) -> Void
let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void
@ -505,6 +507,7 @@ private final class PeerInfoInteraction {
requestLayout: @escaping (Bool) -> Void, requestLayout: @escaping (Bool) -> Void,
openEncryptionKey: @escaping () -> Void, openEncryptionKey: @escaping () -> Void,
openSettings: @escaping (PeerInfoSettingsSection) -> Void, openSettings: @escaping (PeerInfoSettingsSection) -> Void,
openPaymentMethod: @escaping () -> Void,
switchToAccount: @escaping (AccountRecordId) -> Void, switchToAccount: @escaping (AccountRecordId) -> Void,
logoutAccount: @escaping (AccountRecordId) -> Void, logoutAccount: @escaping (AccountRecordId) -> Void,
accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void,
@ -546,6 +549,7 @@ private final class PeerInfoInteraction {
self.requestLayout = requestLayout self.requestLayout = requestLayout
self.openEncryptionKey = openEncryptionKey self.openEncryptionKey = openEncryptionKey
self.openSettings = openSettings self.openSettings = openSettings
self.openPaymentMethod = openPaymentMethod
self.switchToAccount = switchToAccount self.switchToAccount = switchToAccount
self.logoutAccount = logoutAccount self.logoutAccount = logoutAccount
self.accountContextMenu = accountContextMenu self.accountContextMenu = accountContextMenu
@ -568,6 +572,7 @@ private enum SettingsSection: Int, CaseIterable {
case proxy case proxy
case shortcuts case shortcuts
case advanced case advanced
case payment
case extra case extra
case support case support
} }
@ -717,6 +722,10 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
interaction.openSettings(.language) interaction.openSettings(.language)
})) }))
/*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: "Payment Method", icon: PresentationResourcesSettings.language, action: {
interaction.openPaymentMethod()
}))*/
let stickersLabel: String let stickersLabel: String
if let settings = data.globalSettings { if let settings = data.globalSettings {
stickersLabel = settings.unreadTrendingStickerPacks > 0 ? "\(settings.unreadTrendingStickerPacks)" : "" stickersLabel = settings.unreadTrendingStickerPacks > 0 ? "\(settings.unreadTrendingStickerPacks)" : ""
@ -1785,6 +1794,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
openSettings: { [weak self] section in openSettings: { [weak self] section in
self?.openSettings(section: section) self?.openSettings(section: section)
}, },
openPaymentMethod: { [weak self] in
self?.openPaymentMethod()
},
switchToAccount: { [weak self] accountId in switchToAccount: { [weak self] accountId in
self?.switchToAccount(id: accountId) 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) { private func openFaq(anchor: String? = nil) {
let presentationData = self.presentationData let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in let progressSignal = Signal<Never, NoError> { [weak self] subscriber in

View File

@ -254,7 +254,7 @@ private final class TranslateScreenComponent: CombinedComponent {
} }
let originalTitle = originalTitle.update( let originalTitle = originalTitle.update(
component: MultilineTextComponent( 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, horizontalAlignment: .natural,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
@ -264,7 +264,7 @@ private final class TranslateScreenComponent: CombinedComponent {
let originalText = originalText.update( let originalText = originalText.update(
component: MultilineTextComponent( 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, horizontalAlignment: .natural,
maximumNumberOfLines: state.textExpanded ? 0 : 1, maximumNumberOfLines: state.textExpanded ? 0 : 1,
lineSpacing: 0.1 lineSpacing: 0.1
@ -276,7 +276,7 @@ private final class TranslateScreenComponent: CombinedComponent {
let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? "" let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? ""
let translationTitle = translationTitle.update( let translationTitle = translationTitle.update(
component: MultilineTextComponent( 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, horizontalAlignment: .natural,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
@ -291,7 +291,7 @@ private final class TranslateScreenComponent: CombinedComponent {
if let translatedText = state.translatedText { if let translatedText = state.translatedText {
maybeTranslationText = translationText.update( maybeTranslationText = translationText.update(
component: MultilineTextComponent( 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, horizontalAlignment: .natural,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
lineSpacing: 0.1 lineSpacing: 0.1

View File

@ -77,6 +77,7 @@ public enum ParsedInternalUrl {
case peerId(PeerId) case peerId(PeerId)
case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?)
case stickerPack(String) case stickerPack(String)
case invoice(String)
case join(String) case join(String)
case localization(String) case localization(String)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) 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 { } else if pathComponents.count == 2 || pathComponents.count == 3 {
if pathComponents[0] == "addstickers" { if pathComponents[0] == "addstickers" {
return .stickerPack(pathComponents[1]) return .stickerPack(pathComponents[1])
} else if pathComponents[0] == "invoice" {
return .invoice(pathComponents[1])
} else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" { } else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" {
return .join(pathComponents[1]) return .join(pathComponents[1])
} else if pathComponents[0] == "setlanguage" { } else if pathComponents[0] == "setlanguage" {
@ -558,6 +561,18 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
} }
case let .stickerPack(name): case let .stickerPack(name):
return .single(.stickerPack(name: name)) return .single(.stickerPack(name: name))
case let .invoice(slug):
return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramMediaInvoice?, NoError> 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): case let .join(link):
return .single(.join(link)) return .single(.join(link))
case let .localization(identifier): case let .localization(identifier):

View File

@ -22,6 +22,9 @@ swift_library(
"//submodules/Svg:Svg", "//submodules/Svg:Svg",
"//submodules/GZip:GZip", "//submodules/GZip:GZip",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,6 +12,9 @@ import FastBlur
import Svg import Svg
import GZip import GZip
import AppBundle import AppBundle
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import HierarchyTrackingLayer
private let motionAmount: CGFloat = 32.0 private let motionAmount: CGFloat = 32.0
@ -428,6 +431,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
} }
private static var cachedSharedPattern: (PatternKey, UIImage)? private static var cachedSharedPattern: (PatternKey, UIImage)?
private var inlineAnimationNodes: [(AnimatedStickerNode, CGPoint)] = []
private let hierarchyTrackingLayer = HierarchyTrackingLayer()
private var activateInlineAnimationTimer: SwiftSignalKit.Timer?
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true) private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
var isReady: Signal<Bool, NoError> { var isReady: Signal<Bool, NoError> {
return self._isReady.get() return self._isReady.get()
@ -453,7 +460,47 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.addSubnode(self.contentNode) self.addSubnode(self.contentNode)
self.addSubnode(self.patternImageNode) 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 { deinit {
@ -599,6 +646,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.patternImageNode.layer.compositingFilter = "softLightBlendMode" self.patternImageNode.layer.compositingFilter = "softLightBlendMode"
} }
} }
self.patternImageNode.isHidden = false self.patternImageNode.isHidden = false
let invertPattern = intensity < 0 let invertPattern = intensity < 0
if invertPattern { if invertPattern {
@ -676,7 +724,23 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
guard let strongSelf = self else { guard let strongSelf = self else {
return 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.validPatternImage = ValidPatternImage(wallpaper: wallpaper, generate: generator)
strongSelf.validPatternGeneratedImage = nil strongSelf.validPatternGeneratedImage = nil
if let size = strongSelf.validLayout { if let size = strongSelf.validLayout {
@ -796,6 +860,13 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.loadPatternForSizeIfNeeded(size: size, transition: transition) 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 { if isFirstLayout && !self.frame.isEmpty {
self.updateScale() self.updateScale()
} }