Files
Swiftgram/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift
Ilya Laktyushin 2edce5ebf2 Alerts
2025-12-22 00:19:11 +04:00

314 lines
13 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import CheckNode
import Markdown
import TextFormat
import StarsBalanceOverlayComponent
import AlertComponent
import AlertCheckComponent
public class ChatMessagePaymentAlertController: AlertScreen {
private let context: AccountContext?
private let presentationData: PresentationData
private weak var parentNavigationController: NavigationController?
private let chatPeerId: EnginePeer.Id
private let showBalance: Bool
private let animateBalanceOverlay: Bool
private var didUpdateCurrency = false
public var currency: CurrencyAmount.Currency {
didSet {
self.didUpdateCurrency = true
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
}
private let balance = ComponentView<Empty>()
private var didAppear = false
public init(
context: AccountContext?,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
configuration: Configuration = AlertScreen.Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [AlertScreen.Action],
navigationController: NavigationController?,
chatPeerId: EnginePeer.Id,
showBalance: Bool = true,
currency: CurrencyAmount.Currency = .stars,
animateBalanceOverlay: Bool = true
) {
self.context = context
self.presentationData = presentationData
self.parentNavigationController = navigationController
self.chatPeerId = chatPeerId
self.showBalance = showBalance
self.currency = currency
self.animateBalanceOverlay = animateBalanceOverlay
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData))
}
super.init(
configuration: configuration,
content: content,
actions: actions,
updatedPresentationData: effectiveUpdatedPresentationData
)
}
required public init(coder aDecoder: NSCoder) {
preconditionFailure()
}
override public func dismiss(completion: (() -> Void)? = nil) {
super.dismiss(completion: completion)
self.animateOut()
}
private func animateOut() {
if !self.animateBalanceOverlay {
if self.currency == .ton && self.didUpdateCurrency {
self.currency = .stars
}
} else {
if let view = self.balance.view {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
}
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didAppear {
self.didAppear = true
if !layout.metrics.isTablet && layout.size.width > layout.size.height {
Queue.mainQueue().after(0.1) {
self.view.window?.endEditing(true)
}
}
}
if let context = self.context, let _ = self.parentNavigationController, self.showBalance {
let insets = layout.insets(options: .statusBar)
var balanceTransition = ComponentTransition(transition)
if self.balance.view == nil {
balanceTransition = .immediate
}
let balanceSize = self.balance.update(
transition: balanceTransition,
component: AnyComponent(
StarsBalanceOverlayComponent(
context: context,
peerId: self.chatPeerId.namespace == Namespaces.Peer.CloudChannel ? self.chatPeerId : context.account.peerId,
theme: self.presentationData.theme,
currency: self.currency,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else {
return
}
switch self.currency {
case .stars:
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let controller = context.sharedContext.makeStarsPurchaseScreen(
context: context,
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: nil,
customTheme: nil,
completion: { _ in }
)
navigationController.pushViewController(controller)
})
case .ton:
var fragmentUrl = "https://fragment.com/ads/topup"
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String {
fragmentUrl = value
}
context.sharedContext.applicationBindings.openUrl(fragmentUrl)
}
self.dismiss(completion: nil)
}
)
),
environment: {},
containerSize: layout.size
)
if let view = self.balance.view {
if view.superview == nil {
self.view.addSubview(view)
if self.animateBalanceOverlay {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
balanceTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize))
}
}
}
}
public func chatMessagePaymentAlertController(
context: AccountContext?,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peers: [EngineRenderedPeer],
count: Int32,
amount: StarsAmount,
totalAmount: StarsAmount?,
hasCheck: Bool = true,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> ViewController {
let strings = presentationData.strings
let messagesString = strings.Chat_PaidMessage_Confirm_Text_Messages(count)
let text: String
if peers.count == 1, let peer = peers.first {
let amountString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
if case let .channel(channel) = peer.chatOrMonoforumMainPeer, case .broadcast = channel.info {
text = strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
} else {
text = strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
}
} else {
let amount = totalAmount ?? amount
let usersString = strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
text = strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
}
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Chat_PaidMessage_Confirm_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if hasCheck {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.Chat_PaidMessage_Confirm_DontAskAgain, initialValue: false, externalState: checkState)
)
))
}
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
updatedPresentationData: updatedPresentationData,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.Chat_PaidMessage_Confirm_PayForMessage(count), type: .default, action: {
completion(checkState.value)
}),
.init(title: strings.Common_Cancel)
],
navigationController: navigationController,
chatPeerId: context?.account.peerId ?? peers[0].peerId
)
return alertController
}
public func chatMessageRemovePaymentAlertController(
context: AccountContext? = nil,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: EnginePeer,
chatPeer: EnginePeer,
amount: StarsAmount?,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> ViewController {
let strings = presentationData.strings
let text: String
if case .user = chatPeer {
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
} else if let context, chatPeer.id != context.account.peerId {
text = strings.Channel_RemoveFeeAlert_Text(peer.compactDisplayTitle).string
} else {
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
}
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Chat_PaidMessage_RemoveFee_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if let amount {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: amount.value))).string, initialValue: false, externalState: checkState)
)
))
}
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
updatedPresentationData: updatedPresentationData,
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.Chat_PaidMessage_RemoveFee_Yes, type: .default, action: {
completion(checkState.value)
})
],
navigationController: navigationController,
chatPeerId: chatPeer.id
)
return alertController
}