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 wallpaper(WallpaperUrlParameter)
case theme(String)
#if ENABLE_WALLET
case wallet(address: String, amount: Int64?, comment: String?)
#endif
case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?)
case importStickers
case startAttach(peerId: PeerId, payload: String?)
case invoice(slug: String, invoice: TelegramMediaInvoice)
}
public enum NavigateToChatKeepStack {

View File

@ -24,7 +24,7 @@ public final class BotCheckoutController: ViewController {
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 themeParams: [String: Any] = [
"bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb),
@ -34,13 +34,13 @@ public final class BotCheckoutController: ViewController {
"button_text_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.foregroundColor.argb)
]
return context.engine.payments.fetchBotPaymentForm(messageId: messageId, themeParams: themeParams)
return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams)
|> mapError { _ -> FetchError in
return .generic
}
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
if let current = paymentForm.savedInfo {
return context.engine.payments.validateBotPaymentForm(saveInfo: true, messageId: messageId, formInfo: current)
return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current)
|> mapError { _ -> FetchError in
return .generic
}
@ -77,7 +77,7 @@ public final class BotCheckoutController: ViewController {
private let context: AccountContext
private let invoice: TelegramMediaInvoice
private let messageId: EngineMessage.Id
private let source: BotPaymentInvoiceSource
private let completed: (String, EngineMessage.Id?) -> Void
private var presentationData: PresentationData
@ -86,10 +86,10 @@ public final class BotCheckoutController: ViewController {
private let inputData: Promise<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.invoice = invoice
self.messageId = messageId
self.source = source
self.inputData = inputData
self.completed = completed
@ -113,7 +113,7 @@ public final class BotCheckoutController: ViewController {
}
override public func loadDisplayNode() {
let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, messageId: self.messageId, inputData: self.inputData, present: { [weak self] c, a in
let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, source: self.source, inputData: self.inputData, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, dismissAnimated: { [weak self] in
self?.dismiss()

View File

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

View File

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

View File

@ -96,7 +96,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
private let context: AccountContext
private weak var navigationBar: NavigationBar?
private let invoice: BotPaymentInvoice
private let messageId: EngineMessage.Id
private let source: BotPaymentInvoiceSource
private var focus: BotCheckoutInfoControllerFocus?
private let dismiss: () -> Void
@ -130,7 +130,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
context: AccountContext,
navigationBar: NavigationBar?,
invoice: BotPaymentInvoice,
messageId: EngineMessage.Id,
source: BotPaymentInvoiceSource,
formInfo: BotPaymentRequestedInfo,
focus: BotCheckoutInfoControllerFocus,
theme: PresentationTheme,
@ -144,7 +144,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
self.context = context
self.navigationBar = navigationBar
self.invoice = invoice
self.messageId = messageId
self.source = source
self.formInfo = formInfo
self.focus = focus
self.dismiss = dismiss
@ -367,7 +367,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi
func verify() {
self.isVerifying = true
let formInfo = self.collectFormInfo()
self.verifyDisposable.set((self.context.engine.payments.validateBotPaymentForm(saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in
self.verifyDisposable.set((self.context.engine.payments.validateBotPaymentForm(saveInfo: self.saveInfoItem.isOn, source: self.source, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.formInfoUpdated(formInfo, result)
}

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? {
return nil
}
public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
public final class ComponentHostView<EnvironmentType>: UIView {
private var currentComponent: AnyComponent<EnvironmentType>?
private var currentContainerSize: CGSize?
private var currentSize: CGSize?
@ -43,9 +43,7 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
self.isUpdating = true
precondition(containerSize.width.isFinite)
precondition(containerSize.width < .greatestFiniteMagnitude)
precondition(containerSize.height.isFinite)
precondition(containerSize.height < .greatestFiniteMagnitude)
let componentView: UIView
if let current = self.componentView {
@ -62,8 +60,9 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
if updateEnvironment {
EnvironmentBuilder._environment = context.erasedEnvironment
let _ = maybeEnvironment()
let environmentResult = maybeEnvironment()
EnvironmentBuilder._environment = nil
context.erasedEnvironment = environmentResult
}
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 = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Markdown:Markdown",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -120,11 +120,14 @@ open class ViewControllerComponentContainer: ViewController {
}
}
public final class AnimateInTransition {
}
public final class Node: ViewControllerTracingNode {
private var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer?
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
private var component: AnyComponent<ViewControllerComponentContainer.Environment>
private let theme: PresentationTheme?
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)
}
func updateIsVisible(isVisible: Bool) {
func updateIsVisible(isVisible: Bool, animated: Bool) {
if self.currentIsVisible == isVisible {
return
}
@ -180,7 +183,16 @@ open class ViewControllerComponentContainer: ViewController {
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate)
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? Transition(animation: .none).withUserData(AnimateInTransition()) : .immediate)
}
func updateComponent(component: AnyComponent<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) {
super.viewDidAppear(animated)
self.node.updateIsVisible(isVisible: true)
self.node.updateIsVisible(isVisible: true, animated: true)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.node.updateIsVisible(isVisible: false)
self.node.updateIsVisible(isVisible: false, animated: false)
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -238,4 +250,8 @@ open class ViewControllerComponentContainer: ViewController {
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
}
public func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: Transition) {
self.node.updateComponent(component: component, transition: transition)
}
}

View File

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

View File

@ -31,6 +31,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
let iconSize: CGSize?
let iconPlacement: IconPlacement
let title: String
let subtitle: String?
let style: ItemListCheckboxItemStyle
let color: ItemListCheckboxItemColor
let textColor: TextColor
@ -40,12 +41,13 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
let action: () -> Void
let deleteAction: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) {
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) {
self.presentationData = presentationData
self.icon = icon
self.iconSize = iconSize
self.iconPlacement = iconPlacement
self.title = title
self.subtitle = subtitle
self.style = style
self.color = color
self.textColor = textColor
@ -111,6 +113,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
private let imageNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private var item: ItemListCheckboxItem?
@ -149,6 +152,11 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
@ -161,6 +169,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
self.contentContainerNode.addSubnode(self.imageNode)
self.contentContainerNode.addSubnode(self.iconNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.subtitleNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
@ -171,6 +180,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
public func asyncLayout() -> (_ item: ItemListCheckboxItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.item
@ -181,7 +191,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
case .left:
leftInset += 62.0
case .right:
leftInset += 16.0
leftInset += 0.0
}
let iconInset: CGFloat = 62.0
@ -195,8 +205,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
}
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let titleColor: UIColor
let subtitleColor: UIColor = item.presentationData.theme.list.itemSecondaryTextColor
switch item.textColor {
case .primary:
titleColor = item.presentationData.theme.list.itemPrimaryTextColor
@ -206,10 +218,15 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle ?? "", font: subtitleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 28.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
var contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0)
if item.subtitle != nil {
contentSize.height += subtitleLayout.size.height
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -257,6 +274,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
}
let _ = titleApply()
let _ = subtitleApply()
if let image = strongSelf.iconNode.image {
switch item.style {
@ -313,6 +331,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY), size: subtitleLayout.size)
if let icon = item.icon {
let iconSize = item.iconSize ?? icon.size

View File

@ -4,7 +4,7 @@ import UIKit
private let controlStartCharactersSet = CharacterSet(charactersIn: "[*")
private let controlCharactersSet = CharacterSet(charactersIn: "[]()*_-\\")
public final class MarkdownAttributeSet {
public final class MarkdownAttributeSet: Equatable {
public let font: UIFont
public let textColor: UIColor
public let additionalAttributes: [String: Any]
@ -14,9 +14,19 @@ public final class MarkdownAttributeSet {
self.textColor = textColor
self.additionalAttributes = additionalAttributes
}
public static func ==(lhs: MarkdownAttributeSet, rhs: MarkdownAttributeSet) -> Bool {
if !lhs.font.isEqual(rhs.font) {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
return true
}
}
public final class MarkdownAttributes {
public final class MarkdownAttributes: Equatable {
public let body: MarkdownAttributeSet
public let bold: MarkdownAttributeSet
public let link: MarkdownAttributeSet
@ -28,6 +38,19 @@ public final class MarkdownAttributes {
self.bold = bold
self.linkAttribute = linkAttribute
}
public static func ==(lhs: MarkdownAttributes, rhs: MarkdownAttributes) -> Bool {
if lhs.body != rhs.body {
return false
}
if lhs.bold != rhs.bold {
return false
}
if lhs.link != rhs.link {
return false
}
return true
}
}
public func escapedPlaintextForMarkdown(_ string: String) -> String {

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(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center),
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
@ -228,7 +228,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let bottomText = Condition(mode == .create) {
bottomText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center),
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
@ -290,7 +290,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
if let credentials = context.state.credentials {
let credentialsURLTitle = credentialsURLTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left),
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
@ -300,7 +300,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsKeyTitle = credentialsKeyTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left),
text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
@ -310,7 +310,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsURLText = credentialsURLText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: credentials.url, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left),
text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
@ -321,7 +321,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let credentialsKeyText = credentialsKeyText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: credentials.streamKey, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left),
text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1

View File

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

View File

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

View File

@ -2,6 +2,7 @@
#import <Stripe/STPAddress.h>
#import <Stripe/STPPaymentCardTextField.h>
#import <Stripe/STPFormTextField.h>
#import <Stripe/STPAPIClient.h>
#import <Stripe/STPAPIClient+ApplePay.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[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) }
dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) }
dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) }
dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) }
dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) }
dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) }
dict[860303448] = { return Api.InputMedia.parse_inputMediaDocument($0) }
@ -987,7 +989,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) }
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
dict[378828315] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
dict[-1340916937] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
@ -1280,6 +1283,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.InputGroupCall:
_1.serialize(buffer, boxed)
case let _1 as Api.InputInvoice:
_1.serialize(buffer, boxed)
case let _1 as Api.InputMedia:
_1.serialize(buffer, boxed)
case let _1 as Api.InputMessage:
@ -1740,6 +1745,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.payments.BankCardData:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.ExportedInvoice:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentForm:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentReceipt:

View File

@ -647,18 +647,57 @@ public extension Api.payments {
}
}
public extension Api.payments {
enum PaymentForm: TypeConstructorDescription {
case paymentForm(flags: Int32, formId: Int64, botId: Int64, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User])
enum ExportedInvoice: TypeConstructorDescription {
case exportedInvoice(url: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
case .exportedInvoice(let url):
if boxed {
buffer.appendInt32(378828315)
buffer.appendInt32(-1362048039)
}
serializeString(url, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .exportedInvoice(let url):
return ("exportedInvoice", [("url", String(describing: url))])
}
}
public static func parse_exportedInvoice(_ reader: BufferReader) -> ExportedInvoice? {
var _1: String?
_1 = parseString(reader)
let _c1 = _1 != nil
if _c1 {
return Api.payments.ExportedInvoice.exportedInvoice(url: _1!)
}
else {
return nil
}
}
}
}
public extension Api.payments {
enum PaymentForm: TypeConstructorDescription {
case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
if boxed {
buffer.appendInt32(-1340916937)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(formId, buffer: buffer, boxed: false)
serializeInt64(botId, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(description, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)}
invoice.serialize(buffer, true)
serializeInt64(providerId, buffer: buffer, boxed: false)
serializeString(url, buffer: buffer, boxed: false)
@ -677,8 +716,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))])
case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users):
return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))])
}
}
@ -689,45 +728,56 @@ public extension Api.payments {
_2 = reader.readInt64()
var _3: Int64?
_3 = reader.readInt64()
var _4: Api.Invoice?
var _4: String?
_4 = parseString(reader)
var _5: String?
_5 = parseString(reader)
var _6: Api.WebDocument?
if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.WebDocument
} }
var _7: Api.Invoice?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Invoice
_7 = Api.parse(reader, signature: signature) as? Api.Invoice
}
var _5: Int64?
_5 = reader.readInt64()
var _6: String?
_6 = parseString(reader)
var _7: String?
if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
var _8: Api.DataJSON?
var _8: Int64?
_8 = reader.readInt64()
var _9: String?
_9 = parseString(reader)
var _10: String?
if Int(_1!) & Int(1 << 4) != 0 {_10 = parseString(reader) }
var _11: Api.DataJSON?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.DataJSON
_11 = Api.parse(reader, signature: signature) as? Api.DataJSON
} }
var _9: Api.PaymentRequestedInfo?
var _12: Api.PaymentRequestedInfo?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo
_12 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo
} }
var _10: Api.PaymentSavedCredentials?
var _13: Api.PaymentSavedCredentials?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials
_13 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials
} }
var _11: [Api.User]?
var _14: [Api.User]?
if let _ = reader.readInt32() {
_11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil
let _c11 = _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, invoice: _4!, providerId: _5!, url: _6!, nativeProvider: _7, nativeParams: _8, savedInfo: _9, savedCredentials: _10, users: _11!)
let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 0) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 1) == 0) || _13 != nil
let _c14 = _14 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, savedInfo: _12, savedCredentials: _13, users: _14!)
}
else {
return nil

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

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

View File

@ -286,7 +286,7 @@ final class MediaStreamVideoComponent: Component {
let noSignalSize = noSignalView.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: NSAttributedString(string: component.isAdmin ? presentationData.strings.LiveStream_NoSignalAdminText : presentationData.strings.LiveStream_NoSignalUserText(component.peerTitle).string, font: Font.regular(16.0), textColor: .white, paragraphAlignment: .center),
text: .plain(NSAttributedString(string: component.isAdmin ? presentationData.strings.LiveStream_NoSignalAdminText : presentationData.strings.LiveStream_NoSignalUserText(component.peerTitle).string, font: Font.regular(16.0), textColor: .white, paragraphAlignment: .center)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),

View File

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

View File

@ -12,17 +12,21 @@ public extension TelegramEngine {
public func getBankCardInfo(cardNumber: String) -> Signal<BankCardInfo?, NoError> {
return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber)
}
public func fetchBotPaymentForm(messageId: MessageId, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> {
return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, messageId: messageId, themeParams: themeParams)
public func fetchBotPaymentInvoice(source: BotPaymentInvoiceSource) -> Signal<TelegramMediaInvoice, BotPaymentFormRequestError> {
return _internal_fetchBotPaymentInvoice(postbox: self.account.postbox, network: self.account.network, source: source)
}
public func fetchBotPaymentForm(source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal<BotPaymentForm, BotPaymentFormRequestError> {
return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, source: source, themeParams: themeParams)
}
public func validateBotPaymentForm(saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> {
return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, source: source, formInfo: formInfo)
}
public func validateBotPaymentForm(saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal<BotPaymentValidatedFormInfo, ValidateBotPaymentFormError> {
return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, messageId: messageId, formInfo: formInfo)
}
public func sendBotPaymentForm(messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
return _internal_sendBotPaymentForm(account: self.account, messageId: messageId, formId: formId, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials)
public func sendBotPaymentForm(source: BotPaymentInvoiceSource, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal<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> {

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))
} else {
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)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId, inputData: inputData, completed: { currencyValue, receiptMessageId in
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: inputData, completed: { currencyValue, receiptMessageId in
guard let strongSelf = self else {
return
}

View File

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

View File

@ -26,6 +26,7 @@ import ImportStickerPackUI
import PeerInfoUI
import Markdown
import WebUI
import BotPaymentsUI
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation {
@ -574,5 +575,28 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
})
}
})
case let .invoice(slug, invoice):
dismissInput()
if let navigationController = navigationController {
let inputData = Promise<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)"
}
}
} else if parsedUrl.host == "invoice" {
if let components = URLComponents(string: "/?" + query) {
var slug: String?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "slug" {
slug = value
}
}
}
}
if let slug = slug {
convertedUrl = "https://t.me/invoice/\(slug)"
}
}
} else if parsedUrl.host == "setlanguage" {
if let components = URLComponents(string: "/?" + query) {
var lang: String?

View File

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

View File

@ -254,7 +254,7 @@ private final class TranslateScreenComponent: CombinedComponent {
}
let originalTitle = originalTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural),
text: .plain(NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 1
),
@ -264,7 +264,7 @@ private final class TranslateScreenComponent: CombinedComponent {
let originalText = originalText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural),
text: .plain(NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: state.textExpanded ? 0 : 1,
lineSpacing: 0.1
@ -276,7 +276,7 @@ private final class TranslateScreenComponent: CombinedComponent {
let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? ""
let translationTitle = translationTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural),
text: .plain(NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 1
),
@ -291,7 +291,7 @@ private final class TranslateScreenComponent: CombinedComponent {
if let translatedText = state.translatedText {
maybeTranslationText = translationText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural),
text: .plain(NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.1

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import FastBlur
import Svg
import GZip
import AppBundle
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import HierarchyTrackingLayer
private let motionAmount: CGFloat = 32.0
@ -427,6 +430,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
var isLight: Bool
}
private static var cachedSharedPattern: (PatternKey, UIImage)?
private var inlineAnimationNodes: [(AnimatedStickerNode, CGPoint)] = []
private let hierarchyTrackingLayer = HierarchyTrackingLayer()
private var activateInlineAnimationTimer: SwiftSignalKit.Timer?
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
var isReady: Signal<Bool, NoError> {
@ -453,7 +460,47 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.addSubnode(self.contentNode)
self.addSubnode(self.patternImageNode)
//self.view.addSubview(self.bakedBackgroundView)
let animationList: [(String, CGPoint)] = [
("ptrnCAT_1162_1918", CGPoint(x: 1162 - 256, y: 1918 - 256)),
("ptrnDOG_0440_2284", CGPoint(x: 440 - 256, y: 2284 - 256)),
("ptrnGLOB_0438_1553", CGPoint(x: 438 - 256, y: 1553 - 256)),
("ptrnSLON_0906_1033", CGPoint(x: 906 - 256, y: 1033 - 256))
]
for (animation, relativePosition) in animationList {
let animationNode = AnimatedStickerNode()
animationNode.automaticallyLoadFirstFrame = true
animationNode.autoplay = true
self.inlineAnimationNodes.append((animationNode, relativePosition))
self.patternImageNode.addSubnode(animationNode)
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animation), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
}
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
for (animationNode, _) in strongSelf.inlineAnimationNodes {
animationNode.visibility = true
}
strongSelf.activateInlineAnimationTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: {
guard let strongSelf = self else {
return
}
strongSelf.inlineAnimationNodes[Int.random(in: 0 ..< strongSelf.inlineAnimationNodes.count)].0.play()
}, queue: .mainQueue())
strongSelf.activateInlineAnimationTimer?.start()
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
for (animationNode, _) in strongSelf.inlineAnimationNodes {
animationNode.visibility = false
}
strongSelf.activateInlineAnimationTimer?.invalidate()
}
}
deinit {
@ -599,6 +646,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
self.patternImageNode.layer.compositingFilter = "softLightBlendMode"
}
}
self.patternImageNode.isHidden = false
let invertPattern = intensity < 0
if invertPattern {
@ -676,7 +724,23 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
guard let strongSelf = self else {
return
}
if let generator = generator {
if var generator = generator {
generator = { arguments in
let scale = arguments.scale ?? UIScreenScale
let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true)
context.withFlippedContext { c in
if let path = getAppBundle().path(forResource: "PATTERN_static", ofType: "svg"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
if let image = drawSvgImage(data, CGSize(width: arguments.drawingSize.width * scale, height: arguments.drawingSize.height * scale), .clear, .black, false) {
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize))
}
}
}
return context
}
strongSelf.validPatternImage = ValidPatternImage(wallpaper: wallpaper, generate: generator)
strongSelf.validPatternGeneratedImage = nil
if let size = strongSelf.validLayout {
@ -795,6 +859,13 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode
}
self.loadPatternForSizeIfNeeded(size: size, transition: transition)
for (animationNode, relativePosition) in self.inlineAnimationNodes {
let sizeNorm = CGSize(width: 1440, height: 2960)
let animationSize = CGSize(width: 512.0 / sizeNorm.width * size.width, height: 512.0 / sizeNorm.height * size.height)
animationNode.frame = CGRect(origin: CGPoint(x: relativePosition.x / sizeNorm.width * size.width, y: relativePosition.y / sizeNorm.height * size.height), size: animationSize)
animationNode.updateLayout(size: animationNode.frame.size)
}
if isFirstLayout && !self.frame.isEmpty {
self.updateScale()