import Foundation import UIKit import SwiftSignalKit import Display import ComponentFlow import AnimatedTextComponent import PlainButtonComponent import BundleIconComponent import MultilineTextComponent import TelegramPresentationData import TelegramStringFormatting import WalletContext public final class WalletTransferCardComponent: Component { public let amount: Int64 public let recipient: String public let fiatCurrency: WalletContext.FiatCurrency public let fiatRate: WalletContext.FiatRate? public let dateTimeFormat: PresentationDateTimeFormat public let amountText: String? public let recipientTitle: String? public let infoPressed: () -> Void public init( amount: Int64, recipient: String, fiatCurrency: WalletContext.FiatCurrency, fiatRate: WalletContext.FiatRate?, dateTimeFormat: PresentationDateTimeFormat, amountText: String? = nil, recipientTitle: String? = nil, infoPressed: @escaping () -> Void = {} ) { self.amount = amount self.recipient = recipient self.fiatCurrency = fiatCurrency self.fiatRate = fiatRate self.dateTimeFormat = dateTimeFormat self.amountText = amountText self.recipientTitle = recipientTitle self.infoPressed = infoPressed } public static func ==(lhs: WalletTransferCardComponent, rhs: WalletTransferCardComponent) -> Bool { return lhs.amount == rhs.amount && lhs.recipient == rhs.recipient && lhs.fiatCurrency == rhs.fiatCurrency && lhs.fiatRate == rhs.fiatRate && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.amountText == rhs.amountText && lhs.recipientTitle == rhs.recipientTitle } public final class View: UIView { private let backgroundView = WalletCardBackgroundView() private let integralAmount = ComponentView() private let fractionalAmount = ComponentView() private let currency = ComponentView() private let secondaryAmount = ComponentView() private let address = ComponentView() private let infoButton = ComponentView() private var component: WalletTransferCardComponent? private var displayLink: SharedDisplayLinkDriver.Link? private var deviceMotionDisposable: Disposable? private var reflectionRotation = WalletCardBackgroundRotation() public override var isHidden: Bool { didSet { if self.isHidden != oldValue { self.updateAnimationState() } } } override public init(frame: CGRect) { super.init(frame: frame) self.addSubview(self.backgroundView) self.clipsToBounds = true self.layer.cornerRadius = 20.0 if #available(iOS 13.0, *) { self.layer.cornerCurve = .continuous } NotificationCenter.default.addObserver(self, selector: #selector(self.updateAnimationState), name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.stopAnimation), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.updateAnimationState), name: UIAccessibility.reduceMotionStatusDidChangeNotification, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.displayLink?.invalidate() self.deviceMotionDisposable?.dispose() NotificationCenter.default.removeObserver(self) } public override func didMoveToWindow() { super.didMoveToWindow() if self.window != nil { self.reflectionRotation.reset(to: WalletCardBackgroundMotion.shared.currentRotation) } self.updateAnimationState() if self.window != nil { self.backgroundView.renderStaticFrame(reflectionRotation: self.reflectionRotation.value) } } @objc private func updateAnimationState() { guard self.component != nil, self.window != nil, !self.isHidden, UIApplication.shared.applicationState == .active, !UIAccessibility.isReduceMotionEnabled else { self.stopAnimation() return } if self.deviceMotionDisposable == nil { self.deviceMotionDisposable = WalletCardBackgroundMotion.shared.subscribe() self.updateBackgroundMotion(isResuming: true) } if self.displayLink == nil { self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in self?.updateBackgroundMotion() } } self.updateBackgroundMotion() } @objc private func stopAnimation() { self.displayLink?.invalidate() self.displayLink = nil self.deviceMotionDisposable?.dispose() self.deviceMotionDisposable = nil } private func updateBackgroundMotion(isResuming: Bool = false) { guard self.deviceMotionDisposable != nil, let window = self.window else { return } let time = CACurrentMediaTime() let rotation = Double(WalletCardBackgroundMotion.shared.rotation( at: time, orientation: window.windowScene?.interfaceOrientation ?? .portrait )) let previousRotation = self.reflectionRotation.value if isResuming { self.reflectionRotation.resume(at: time, to: rotation) } self.reflectionRotation.update(at: time, to: rotation) guard self.reflectionRotation.value != previousRotation else { return } self.backgroundView.renderStaticFrame(reflectionRotation: self.reflectionRotation.value) } func update( component: WalletTransferCardComponent, availableSize: CGSize, transition: ComponentTransition ) -> CGSize { self.component = component let referenceSize = CGSize(width: 361.0, height: 220.0) let width = max(1.0, availableSize.width) let scale = width / referenceSize.width let size = CGSize(width: width, height: referenceSize.height * scale) self.layer.cornerRadius = 20.0 * width / 336.0 let formattedAmount = component.amountText ?? formatTonAmountText( component.amount, dateTimeFormat: component.dateTimeFormat, maxDecimalPositions: 9 ) let amountIntegralText: String let amountFractionalText: String let isTextAmount = component.amountText?.unicodeScalars.contains(where: CharacterSet.letters.contains) == true if isTextAmount || (component.amount == 0 && component.amountText == nil) { amountIntegralText = formattedAmount amountFractionalText = "" } else if let decimalRange = formattedAmount.range(of: component.dateTimeFormat.decimalSeparator) { amountIntegralText = String(formattedAmount[.. View { return View(frame: .zero) } public func update( view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition ) -> CGSize { return view.update( component: self, availableSize: availableSize, transition: transition ) } } private func formattedWalletTransferAddress(_ address: String) -> String { var groups: [String] = [] var currentIndex = address.startIndex while currentIndex < address.endIndex { let endIndex = address.index(currentIndex, offsetBy: 4, limitedBy: address.endIndex) ?? address.endIndex groups.append(String(address[currentIndex ..< endIndex])) currentIndex = endIndex } let splitIndex = min(6, groups.count) let firstLine = groups[..