2024-08-06 22:40:17 +02:00

824 lines
38 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BalancedTextComponent
import MultilineTextComponent
import BundleIconComponent
import ButtonComponent
import ItemListUI
import UndoUI
import AccountContext
import PresentationDataUtils
import StarsImageComponent
import ConfettiEffect
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let starsContext: StarsContext
let invoice: TelegramMediaInvoice
let source: BotPaymentInvoiceSource
let extendedMedia: [TelegramExtendedMedia]
let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
let dismiss: () -> Void
init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
dismiss: @escaping () -> Void
) {
self.context = context
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
return false
}
return true
}
final class State: ComponentState {
var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedStarImage: (UIImage, PresentationTheme)?
private let context: AccountContext
private let starsContext: StarsContext
private let source: BotPaymentInvoiceSource
private let extendedMedia: [TelegramExtendedMedia]
private let invoice: TelegramMediaInvoice
private(set) var botPeer: EnginePeer?
private(set) var chatPeer: EnginePeer?
private var peerDisposable: Disposable?
private(set) var balance: Int64?
private(set) var form: BotPaymentForm?
private var stateDisposable: Disposable?
private var optionsDisposable: Disposable?
private(set) var options: [StarsTopUpOption] = [] {
didSet {
self.optionsPromise.set(self.options)
}
}
private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil)
var inProgress = false
init(
context: AccountContext,
starsContext: StarsContext,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
invoice: TelegramMediaInvoice,
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
) {
self.context = context
self.starsContext = starsContext
self.source = source
self.extendedMedia = extendedMedia
self.invoice = invoice
super.init()
let chatPeer: Signal<EnginePeer?, NoError>
if case let .message(messageId) = source {
chatPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
} else {
chatPeer = .single(nil)
}
self.peerDisposable = (combineLatest(
inputData,
chatPeer
)
|> deliverOnMainQueue).start(next: { [weak self] inputData, chatPeer in
guard let self else {
return
}
self.balance = inputData?.0.balance ?? 0
self.form = inputData?.1
self.botPeer = inputData?.2
self.chatPeer = chatPeer
self.updated(transition: .immediate)
if self.optionsDisposable == nil, let balance = self.balance, balance < self.invoice.totalAmount {
self.optionsDisposable = (context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
}
})
self.stateDisposable = (starsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
self.balance = state?.balance
self.updated(transition: .immediate)
})
}
deinit {
self.peerDisposable?.dispose()
self.stateDisposable?.dispose()
self.optionsDisposable?.dispose()
}
func buy(requestTopUp: @escaping (@escaping () -> Void) -> Void, completion: @escaping (Bool) -> Void) {
guard let form, let balance else {
return
}
let action = { [weak self] in
guard let self else {
return
}
self.inProgress = true
self.updated()
let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source)
|> deliverOnMainQueue).start(next: { _ in
completion(true)
}, error: { [weak self] error in
guard let self else {
return
}
switch error {
case .alreadyPaid:
if !self.extendedMedia.isEmpty, case let .message(messageId) = self.source {
let _ = self.context.engine.messages.updateExtendedMedia(messageIds: [messageId]).startStandalone()
}
default:
break
}
completion(false)
})
}
if balance < self.invoice.totalAmount {
if self.options.isEmpty {
self.inProgress = true
self.updated()
}
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
if let self {
self.inProgress = false
self.updated()
requestTopUp({ [weak self] in
guard let self else {
return
}
self.inProgress = true
self.updated()
let _ = (self.starsContext.state
|> filter { state in
if let state {
return !state.flags.contains(.isPendingBalance)
}
return false
}
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
Queue.mainQueue().after(0.1, { [weak self] in
if let self, let balance = self.balance, balance < self.invoice.totalAmount {
self.inProgress = false
self.updated()
self.buy(requestTopUp: requestTopUp, completion: completion)
} else {
action()
}
})
})
})
}
})
} else {
action()
}
}
}
func makeState() -> State {
return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData)
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let star = Child(StarsImageComponent.self)
let closeButton = Child(Button.self)
let title = Child(Text.self)
let text = Child(BalancedTextComponent.self)
let button = Child(ButtonComponent.self)
let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self)
let balanceIcon = Child(BundleIconComponent.self)
let info = Child(BalancedTextComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = presentationData.theme
let strings = presentationData.strings
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update(
component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0),
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let subject: StarsImageComponent.Subject
if !component.extendedMedia.isEmpty {
subject = .extendedMedia(component.extendedMedia)
} else if let peer = state.botPeer {
if let photo = component.invoice.photo {
subject = .photo(photo)
} else {
subject = .transactionPeer(.peer(peer))
}
} else {
subject = .none
}
var isSubscription = false
if case .starsChatSubscription = context.component.source {
isSubscription = true
}
let star = star.update(
component: StarsImageComponent(
context: component.context,
subject: subject,
theme: theme,
diameter: 90.0,
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor,
icon: isSubscription ? .star : nil
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0))
)
let closeImage: UIImage
if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: {
component.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0))
)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
contentSize.height += 126.0
let titleString: String
if isSubscription {
//TODO:localize
titleString = "Subscribe to the Channel"
} else {
titleString = strings.Stars_Transfer_Title
}
let title = title.update(
component: Text(text: titleString, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += 13.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let amount = component.invoice.totalAmount
let infoText: String
if case .starsChatSubscription = context.component.source {
infoText = "Do you want to subscribe to **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?"
} else if !component.extendedMedia.isEmpty {
var description: String = ""
var photoCount: Int32 = 0
var videoCount: Int32 = 0
for media in component.extendedMedia {
if case let .preview(_, _, videoDuration) = media, videoDuration != nil {
videoCount += 1
} else {
photoCount += 1
}
}
if photoCount > 0 && videoCount > 0 {
description = strings.Stars_Transfer_MediaAnd("**\(strings.Stars_Transfer_Photos(photoCount))**", "**\(strings.Stars_Transfer_Videos(videoCount))**").string
} else if photoCount > 0 {
if photoCount > 1 {
description += "**\(strings.Stars_Transfer_Photos(photoCount))**"
} else {
description += "**\(strings.Stars_Transfer_SinglePhoto)**"
}
} else if videoCount > 0 {
if videoCount > 1 {
description += "**\(strings.Stars_Transfer_Videos(videoCount))**"
} else {
description += "**\(strings.Stars_Transfer_SingleVideo)**"
}
}
infoText = strings.Stars_Transfer_UnlockInfo(
description,
state.chatPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(amount))
).string
} else {
infoText = strings.Stars_Transfer_Info(
component.invoice.title,
state.botPeer?.compactDisplayTitle ?? "",
strings.Stars_Transfer_Info_Stars(Int32(amount))
).string
}
let text = text.update(
component: BalancedTextComponent(
text: .markdown(
text: infoText,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += 28.0
let balanceTitle = balanceTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Stars_Transfer_Balance,
font: Font.regular(14.0),
textColor: textColor
)),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: .immediate
)
let balanceValue = balanceValue.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(Int32(state.balance ?? 0), environment.dateTimeFormat.groupingSeparator),
font: Font.semibold(16.0),
textColor: textColor
)),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: .immediate
)
let balanceIcon = balanceIcon.update(
component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil),
availableSize: context.availableSize,
transition: .immediate
)
let topBalanceOriginY = 11.0
context.add(balanceTitle
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0))
)
context.add(balanceIcon
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel))
)
context.add(balanceValue
.position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 2.0 - UIScreenPixel))
)
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator)
let buttonAttributedString: NSMutableAttributedString
if case .starsChatSubscription = component.source {
buttonAttributedString = NSMutableAttributedString(string: "Subscribe", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
} else {
buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
}
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
}
let controller = environment.controller() as? StarsTransferScreen
let accountContext = component.context
let starsContext = component.starsContext
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
let invoice = component.invoice
let isMedia = !component.extendedMedia.isEmpty
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: state.inProgress,
action: { [weak state, weak controller] in
state?.buy(requestTopUp: { [weak controller] completion in
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: accountContext.currentAppConfiguration.with { $0 })
if !premiumConfiguration.isPremiumDisabled {
let purpose: StarsPurchasePurpose
if isMedia {
purpose = .unlockMedia(requiredStars: invoice.totalAmount)
} else if let peerId = state?.botPeer?.id {
purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount)
} else {
purpose = .generic(requiredStars: nil)
}
let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen(
context: accountContext,
starsContext: starsContext,
options: state?.options ?? [],
purpose: purpose,
completion: { [weak starsContext] stars in
starsContext?.add(balance: stars)
Queue.mainQueue().after(0.1) {
completion()
}
}
)
controller?.push(purchaseController)
} else {
let alertController = textAlertController(context: accountContext, title: nil, text: presentationData.strings.Stars_Transfer_Unavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller?.present(alertController, in: .window(.root))
}
}, completion: { [weak controller] success in
if success {
let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 }
var title = presentationData.strings.Stars_Transfer_PurchasedTitle
let text: String
if isSubscription {
//TODO:localize
title = "Subscription successful!"
text = "\(presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))) transferred to \(botTitle)."
} else if let _ = component.invoice.extendedMedia {
text = presentationData.strings.Stars_Transfer_UnlockedText( presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string
} else {
text = presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string
}
if let navigationController = controller?.navigationController {
Queue.mainQueue().after(0.5) {
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .universal(
animation: "StarsSend",
scale: 0.066,
colors: [:],
title: title,
text: text,
customUndoText: nil,
timeout: nil
),
elevatedLayout: lastController is ChatController,
action: { _ in return true}
)
lastController.present(resultController, in: .window(.root))
}
}
}
}
controller?.complete(paid: success)
controller?.dismissAnimated()
starsContext.load(force: true)
})
}
),
availableSize: CGSize(width: 361.0, height: 50),
transition: .immediate
)
context.add(button
.clipsToBounds(true)
.cornerRadius(10.0)
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
if isSubscription {
contentSize.height += 14.0
let termsTextFont = Font.regular(13.0)
let termsTextColor = theme.actionSheet.secondaryTextColor
let termsLinkColor = theme.actionSheet.controlAccentColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let info = info.update(
component: BalancedTextComponent(
text: .markdown(
text: strings.Stars_Subscription_Terms,
attributes: termsMarkdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { [weak controller] attributes, _ in
if let controller, let navigationController = controller.navigationController as? NavigationController {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Subscription_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(info
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0))
)
contentSize.height += info.size.height
}
contentSize.height += 48.0
return contentSize
}
}
}
private final class StarsTransferSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
private let context: AccountContext
private let starsContext: StarsContext
private let invoice: TelegramMediaInvoice
private let source: BotPaymentInvoiceSource
private let extendedMedia: [TelegramExtendedMedia]
private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
) {
self.context = context
self.starsContext = starsContext
self.invoice = invoice
self.source = source
self.extendedMedia = extendedMedia
self.inputData = inputData
}
static func ==(lhs: StarsTransferSheetComponent, rhs: StarsTransferSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.invoice != rhs.invoice {
return false
}
if lhs.extendedMedia != rhs.extendedMedia {
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>(SheetContent(
context: context.component.context,
starsContext: context.component.starsContext,
invoice: context.component.invoice,
source: context.component.source,
extendedMedia: context.component.extendedMedia,
inputData: context.component.inputData,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .blur(.light),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
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 StarsTransferScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let extendedMedia: [TelegramExtendedMedia]
private let completion: (Bool) -> Void
public init(
context: AccountContext,
starsContext: StarsContext,
invoice: TelegramMediaInvoice,
source: BotPaymentInvoiceSource,
extendedMedia: [TelegramExtendedMedia] = [],
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>,
completion: @escaping (Bool) -> Void
) {
self.context = context
self.extendedMedia = extendedMedia
self.completion = completion
super.init(
context: context,
component: StarsTransferSheetComponent(
context: context,
starsContext: starsContext,
invoice: invoice,
source: source,
extendedMedia: extendedMedia,
inputData: inputData
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
starsContext.load(force: false)
}
deinit {
self.complete(paid: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var didComplete = false
fileprivate func complete(paid: Bool) {
guard !self.didComplete else {
return
}
self.didComplete = true
self.completion(paid)
if !self.extendedMedia.isEmpty && paid {
self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds, customImage: UIImage(bundleImageName: "Peer Info/PremiumIcon")))
}
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}