mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
6b7ed2e6ac
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
20
submodules/Components/Forms/CreditCardInputComponent/BUILD
Normal file
20
submodules/Components/Forms/CreditCardInputComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
19
submodules/Components/Forms/TextInputComponent/BUILD
Normal file
19
submodules/Components/Forms/TextInputComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
43
submodules/PaymentMethodUI/BUILD
Normal file
43
submodules/PaymentMethodUI/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
449
submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift
Normal file
449
submodules/PaymentMethodUI/Sources/PaymentCardEntryScreen.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
286
submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift
Normal file
286
submodules/PaymentMethodUI/Sources/PaymentMethodListScreen.swift
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -99,6 +99,7 @@ swift_library(
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/FetchManagerImpl:FetchManagerImpl",
|
||||
"//submodules/ListMessageItem:ListMessageItem",
|
||||
"//submodules/PaymentMethodUI:PaymentMethodUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
)),
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
1549
submodules/TelegramUI/Resources/PATTERN_static.svg
Normal file
1549
submodules/TelegramUI/Resources/PATTERN_static.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 127 KiB |
BIN
submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs
Normal file
BIN
submodules/TelegramUI/Resources/ptrnCAT_1162_1918.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs
Normal file
BIN
submodules/TelegramUI/Resources/ptrnDOG_0440_2284.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs
Normal file
BIN
submodules/TelegramUI/Resources/ptrnGLOB_0438_1553.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs
Normal file
BIN
submodules/TelegramUI/Resources/ptrnSLON_0906_1033.tgs
Normal file
Binary file not shown.
@ -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
|
||||
}
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)*/
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user