Swiftgram/submodules/PremiumUI/Sources/PremiumGiftScreen.swift
2023-08-11 02:44:05 +02:00

1146 lines
50 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import PresentationDataUtils
import ViewControllerComponent
import AccountContext
import SolidRoundedButtonComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import BlurredBackgroundComponent
import Markdown
import InAppPurchaseManager
import ConfettiEffect
import TextFormat
import UniversalMediaPlayer
import AttachmentUI
public enum PremiumGiftSource: Equatable {
case profile
case attachMenu
var identifier: String? {
switch self {
case .profile:
return "profile"
case .attachMenu:
return "attach"
}
}
}
private final class PremiumGiftScreenContentComponent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
let context: AccountContext
let source: PremiumGiftSource
let peer: EnginePeer?
let products: [PremiumGiftProduct]?
let selectedProductId: String?
let present: (ViewController) -> Void
let selectProduct: (String) -> Void
let buy: () -> Void
init(context: AccountContext, source: PremiumGiftSource, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) {
self.context = context
self.source = source
self.peer = peer
self.products = products
self.selectedProductId = selectedProductId
self.present = present
self.selectProduct = selectProduct
self.buy = buy
}
static func ==(lhs: PremiumGiftScreenContentComponent, rhs: PremiumGiftScreenContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.products != rhs.products {
return false
}
if lhs.selectedProductId != rhs.selectedProductId {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
private(set) var configuration = PremiumIntroConfiguration.defaultValue
private(set) var promoConfiguration: PremiumPromoConfiguration?
private var stickersDisposable: Disposable?
private var preloadDisposableSet = DisposableSet()
var price: String?
init(context: AccountContext, source: PremiumGiftSource) {
self.context = context
super.init()
self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.App(),
TelegramEngine.EngineData.Item.Configuration.PremiumPromo()
)
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration, promoConfiguration in
if let strongSelf = self {
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
strongSelf.promoConfiguration = promoConfiguration
strongSelf.updated(transition: .immediate)
if let identifier = source.identifier {
var jsonString: String = "{"
jsonString += "\"source\": \"\(identifier)\","
jsonString += "\"data\": {\"premium_promo_order\":["
var isFirst = true
for perk in strongSelf.configuration.perks {
if !isFirst {
jsonString += ","
}
isFirst = false
jsonString += "\"\(perk.identifier)\""
}
jsonString += "]}}"
if let data = jsonString.data(using: .utf8), let json = JSON(data: data) {
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium_gift.promo_screen_show", data: json)
}
}
for (_, video) in promoConfiguration.videos {
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
}
}
})
let _ = updatePremiumPromoConfigurationOnce(account: context.account).start()
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers)
self.stickersDisposable = (self.context.account.postbox.combinedView(keys: [stickersKey])
|> deliverOnMainQueue).start(next: { [weak self] views in
guard let strongSelf = self else {
return
}
if let view = views.views[stickersKey] as? OrderedItemListView {
for item in view.items {
if let mediaItem = item.contents.get(RecentMediaItem.self) {
let file = mediaItem.media
strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
if let effect = file.videoThumbnails.first {
strongSelf.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start())
}
}
}
}
})
}
deinit {
self.disposable?.dispose()
self.preloadDisposableSet.dispose()
self.stickersDisposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, source: self.source)
}
static var body: Body {
let overscroll = Child(Rectangle.self)
let text = Child(MultilineTextComponent.self)
let optionsSection = Child(SectionGroupComponent.self)
let perksSection = Child(SectionGroupComponent.self)
return { context in
let sideInset: CGFloat = 16.0
let component = context.component
let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let theme = environment.theme
let strings = environment.strings
let availableWidth = context.availableSize.width
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
var size = CGSize(width: context.availableSize.width, height: 0.0)
let overscroll = overscroll.update(
component: Rectangle(color: theme.list.plainBackgroundColor),
availableSize: CGSize(width: context.availableSize.width, height: 1000),
transition: context.transition
)
context.add(overscroll
.position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0))
)
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
let textColor = theme.list.itemPrimaryTextColor
let titleColor = theme.list.itemPrimaryTextColor
let subtitleColor = theme.list.itemSecondaryTextColor
let arrowColor = theme.list.disclosureArrowColor
let accentColor = theme.list.itemAccentColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_Gift_Description(component.peer?.compactDisplayTitle ?? "").string,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: 240.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 += 21.0
var items: [SectionGroupComponent.Item] = []
var i = 0
if let products = component.products {
let gradientColors: [UIColor] = [
UIColor(rgb: 0x8e77ff),
UIColor(rgb: 0x9a6fff),
UIColor(rgb: 0xb36eee)
]
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = products.last {
shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months)))
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
for product in products {
let giftTitle: String
if product.months == 12 {
giftTitle = strings.Premium_Gift_Years(1)
} else {
giftTitle = strings.Premium_Gift_Months(product.months)
}
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)) * 100.0)
let discount: String
if discountValue > 0 {
discount = "-\(discountValue)%"
} else {
discount = ""
}
let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(product.months))
var subtitle = ""
var accessibilitySubtitle = ""
var pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months))
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
if discountValue > 0 {
subtitle = "**\(defaultPrice)** \(product.price)"
accessibilitySubtitle = product.price
}
items.append(SectionGroupComponent.Item(
AnyComponentWithIdentity(
id: product.id,
component: AnyComponent(
PremiumOptionComponent(
title: giftTitle,
subtitle: subtitle,
labelPrice: pricePerMonth,
discount: discount,
selected: product.id == component.selectedProductId,
primaryTextColor: textColor,
secondaryTextColor: subtitleColor,
accentColor: gradientColors[i],
checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor,
checkBorderColor: environment.theme.list.itemCheckColors.strokeColor
)
)
),
accessibilityLabel: "\(giftTitle). \(accessibilitySubtitle). \(pricePerMonth)",
action: {
component.selectProduct(product.id)
})
)
i += 1
}
}
let optionsSection = optionsSection.update(
component: SectionGroupComponent(
items: items,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(optionsSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += optionsSection.size.height
size.height += 23.0
let state = context.state
let accountContext = context.component.context
let present = context.component.present
let buy = context.component.buy
let price = context.component.products?.first(where: { $0.id == context.component.selectedProductId })?.price
state.price = price
let gradientColors: [UIColor] = [
UIColor(rgb: 0xF27C30),
UIColor(rgb: 0xE36850),
UIColor(rgb: 0xda5d63),
UIColor(rgb: 0xD15078),
UIColor(rgb: 0xC14998),
UIColor(rgb: 0xB24CB5),
UIColor(rgb: 0xA34ED0),
UIColor(rgb: 0x9054E9),
UIColor(rgb: 0x7561EB),
UIColor(rgb: 0x5A6EEE),
UIColor(rgb: 0x548DFF),
UIColor(rgb: 0x54A3FF),
UIColor(rgb: 0x54bdff),
UIColor(rgb: 0x71c8ff),
UIColor(rgb: 0xa0daff)
]
i = 0
var perksItems: [SectionGroupComponent.Item] = []
for perk in state.configuration.perks {
let iconBackgroundColors = gradientColors[i]
perksItems.append(SectionGroupComponent.Item(
AnyComponentWithIdentity(
id: perk.identifier,
component: AnyComponent(
PerkComponent(
iconName: perk.iconName,
iconBackgroundColors: [
iconBackgroundColors
],
title: perk.title(strings: strings),
titleColor: titleColor,
subtitle: perk.subtitle(strings: strings),
subtitleColor: subtitleColor,
arrowColor: arrowColor,
accentColor: accentColor
)
)
),
accessibilityLabel: "\(perk.title(strings: strings)). \(perk.subtitle(strings: strings))",
action: { [weak state] in
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
}
let buttonText: String
if let price = state?.price {
buttonText = strings.Premium_Gift_GiftSubscription(price).string
} else {
buttonText = strings.Common_OK
}
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .gift(state?.price), order: state?.configuration.perks, buttonText: buttonText, isPremium: false)
controller.action = { [weak state] in
dismissImpl?()
if let _ = state?.price {
buy()
}
}
controller.disposed = {
// updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium_gift.promo_screen_tap", data: ["item": perk.identifier])
}
))
i += 1
}
let perksSection = perksSection.update(
component: SectionGroupComponent(
items: perksItems,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
selectionColor: environment.theme.list.itemHighlightedBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(perksSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + perksSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += perksSection.size.height
size.height += 10.0
size.height += scrollEnvironment.insets.bottom
return size
}
}
}
private struct PremiumGiftProduct: Equatable {
let giftOption: CachedPremiumGiftOption
let storeProduct: InAppPurchaseManager.Product
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.giftOption.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
}
private final class PremiumGiftScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: PeerId
let options: [CachedPremiumGiftOption]
let source: PremiumGiftSource
let buttonStatePromise: Promise<AttachmentMainButtonState?>
let buttonAction: ActionSlot<Void>
let updateInProgress: (Bool) -> Void
let updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void
let present: (ViewController) -> Void
let push: (ViewController) -> Void
let completion: (Int32) -> Void
init(
context: AccountContext,
peerId: PeerId,
options: [CachedPremiumGiftOption],
source: PremiumGiftSource,
buttonStatePromise: Promise<AttachmentMainButtonState?>,
buttonAction: ActionSlot<Void>,
updateInProgress: @escaping (Bool) -> Void,
updateTabBarAlpha: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void,
present: @escaping (ViewController) -> Void,
push: @escaping (ViewController) -> Void,
completion: @escaping (Int32) -> Void)
{
self.context = context
self.peerId = peerId
self.options = options
self.source = source
self.buttonStatePromise = buttonStatePromise
self.buttonAction = buttonAction
self.updateInProgress = updateInProgress
self.updateTabBarAlpha = updateTabBarAlpha
self.present = present
self.push = push
self.completion = completion
}
static func ==(lhs: PremiumGiftScreenComponent, rhs: PremiumGiftScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.source != rhs.source {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let peerId: PeerId
private let options: [CachedPremiumGiftOption]
private let source: PremiumGiftSource
private let buttonStatePromise: Promise<AttachmentMainButtonState?>
private let buttonAction: ActionSlot<Void>
private let updateInProgress: (Bool) -> Void
private let present: (ViewController) -> Void
private let completion: (Int32) -> Void
var topContentOffset: CGFloat?
var bottomContentOffset: CGFloat?
var hasIdleAnimations = true
var inProgress = false {
didSet {
self.updateButton()
}
}
var peer: EnginePeer?
var products: [PremiumGiftProduct]?
var selectedProductId: String?
private var disposable: Disposable?
private var paymentDisposable = MetaDisposable()
private var activationDisposable = MetaDisposable()
init(
context: AccountContext,
peerId: PeerId,
options: [CachedPremiumGiftOption],
source: PremiumGiftSource,
buttonStatePromise: Promise<AttachmentMainButtonState?>,
buttonAction: ActionSlot<Void>,
updateInProgress: @escaping (Bool) -> Void,
present: @escaping (ViewController) -> Void,
completion: @escaping (Int32) -> Void)
{
self.context = context
self.peerId = peerId
self.options = options
self.source = source
self.buttonAction = buttonAction
self.buttonStatePromise = buttonStatePromise
self.updateInProgress = updateInProgress
self.present = present
self.completion = completion
super.init()
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts
} else {
availableProducts = .single([])
}
self.disposable = combineLatest(
queue: Queue.mainQueue(),
availableProducts,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
).start(next: { [weak self] products, peer in
if let strongSelf = self {
var gifts: [PremiumGiftProduct] = []
for option in strongSelf.options {
if let product = products.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
gifts.append(PremiumGiftProduct(giftOption: option, storeProduct: product))
}
}
strongSelf.products = gifts
if strongSelf.selectedProductId == nil && strongSelf.source != .attachMenu {
strongSelf.selectedProductId = strongSelf.products?.first?.id
}
strongSelf.peer = peer
strongSelf.updated(transition: .immediate)
}
})
self.buttonAction.connect({ [weak self] in
self?.buy()
})
}
deinit {
self.disposable?.dispose()
self.paymentDisposable.dispose()
self.activationDisposable.dispose()
}
func selectProduct(id: String) {
self.selectedProductId = id
self.updateButton()
self.updated(transition: .immediate)
}
private func updateButton() {
guard self.source == .attachMenu else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let price: String?
if let products = self.products, let selectedProductId = self.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) {
price = product.price
} else {
price = nil
}
let buttonText = presentationData.strings.Premium_Gift_GiftSubscription(price ?? "").string
self.buttonStatePromise.set(.single(AttachmentMainButtonState(text: buttonText, font: .bold, background: .premium, textColor: .white, isVisible: true, progress: self.inProgress ? .center : .none, isEnabled: true)))
}
func buy() {
guard let inAppPurchaseManager = self.context.inAppPurchaseManager, !self.inProgress else {
return
}
guard let product = self.products?.first(where: { $0.id == self.selectedProductId }) else {
return
}
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
let duration = product.months
addAppLogEvent(postbox: self.context.account.postbox, type: "premium_gift.promo_screen_accept")
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .gift(peerId: self.peerId, currency: currency, amount: amount))
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, targetPeerId: strongSelf.peerId)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status {
Queue.mainQueue().after(2.0) {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion(duration)
}
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText = errorText {
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}
}))
} else {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
}
}
})
}
func updateIsFocused(_ isFocused: Bool) {
self.hasIdleAnimations = !isFocused
self.updated(transition: .immediate)
}
}
func makeState() -> State {
return State(
context: self.context,
peerId: self.peerId,
options: self.options,
source: self.source,
buttonStatePromise: self.buttonStatePromise,
buttonAction: self.buttonAction,
updateInProgress: self.updateInProgress,
present: self.present,
completion: self.completion
)
}
static var body: Body {
let background = Child(Rectangle.self)
let scrollContent = Child(ScrollComponent<EnvironmentType>.self)
let star = Child(GiftAvatarComponent.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let secondaryTitle = Child(MultilineTextComponent.self)
let bottomPanel = Child(BlurredBackgroundComponent.self)
let bottomSeparator = Child(Rectangle.self)
let button = Child(SolidRoundedButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self].value
let state = context.state
let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition)
var starIsVisible = true
if let topContentOffset = state.topContentOffset, topContentOffset >= 123.0 {
starIsVisible = false
}
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: environment.theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: environment.navigationHeight),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: environment.theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Premium_Gift_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let secondaryTitle = secondaryTitle.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Premium_Gift_TitleShort, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight: CGFloat = context.component.source == .attachMenu ? environment.safeInsets.bottom : bottomPanelPadding + 50.0 + bottomInset
let topInset: CGFloat = environment.navigationHeight - 56.0
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumGiftScreenContentComponent(
context: context.component.context,
source: context.component.source,
peer: state.peer,
products: state.products,
selectedProductId: state.selectedProductId,
present: context.component.present,
selectProduct: { [weak state] productId in
state?.selectProduct(id: productId)
}, buy: { [weak state] in
state?.buy()
}
)),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { targetContentOffset in
if targetContentOffset.pointee.y < 100.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0)
} else if targetContentOffset.pointee.y < 123.0 {
targetContentOffset.pointee = CGPoint(x: 0.0, y: 123.0)
}
}
),
environment: { environment },
availableSize: context.availableSize,
transition: context.transition
)
context.add(scrollContent
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let topPanelAlpha: CGFloat
let titleOffset: CGFloat
let titleScale: CGFloat
let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let titleAlpha: CGFloat
if let topContentOffset = state.topContentOffset {
topPanelAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0
let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0
titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
titleScale = 1.0 - fraction * 0.36
titleAlpha = 1.0 - min(1.0, fraction * 1.1)
} else {
topPanelAlpha = 0.0
titleScale = 1.0
titleOffset = 0.0
titleAlpha = 1.0
}
let star = star.update(
component: GiftAvatarComponent(
context: context.component.context,
peer: context.state.peer,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
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: topInset + star.size.height / 2.0 - 30.0 - titleOffset * titleScale))
.scale(titleScale)
)
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(titleAlpha)
)
context.add(secondaryTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(max(0.0, 1.0 - titleAlpha * 1.8))
)
let price: String?
if let products = state.products, let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) {
price = product.price
} else {
price = nil
}
let bottomPanelAlpha: CGFloat
if let bottomContentOffset = state.bottomContentOffset {
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
} else {
bottomPanelAlpha = 1.0
}
if context.component.source == .attachMenu {
context.component.updateTabBarAlpha(bottomPanelAlpha, .immediate)
} else {
let sideInset: CGFloat = 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: environment.strings.Premium_Gift_GiftSubscription(price ?? "").string,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
height: 50.0,
cornerRadius: 11.0,
gloss: true,
isLoading: state.inProgress,
action: {
state.buy()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0),
transition: context.transition)
let bottomPanel = bottomPanel.update(
component: BlurredBackgroundComponent(
color: environment.theme.rootController.tabBar.backgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset),
transition: context.transition
)
let bottomSeparator = bottomSeparator.update(
component: Rectangle(
color: environment.theme.rootController.tabBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
context.add(bottomPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
.opacity(bottomPanelAlpha)
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
)
}
return context.availableSize
}
}
}
public final class PremiumGiftScreen: ViewControllerComponentContainer, AttachmentContainable {
fileprivate let context: AccountContext
private var didSetReady = false
private let _ready = Promise<Bool>()
public override var ready: Promise<Bool> {
return self._ready
}
public weak var sourceView: UIView?
public weak var containerView: UIView?
public var animationColor: UIColor?
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonActionSlot = ActionSlot<Void>()
public init(context: AccountContext, peerId: PeerId, options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
var updateInProgressImpl: ((Bool) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var completionImpl: ((Int32) -> Void)?
var updateTabBarAlphaImpl: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
super.init(context: context, component: PremiumGiftScreenComponent(
context: context,
peerId: peerId,
options: options,
source: source,
buttonStatePromise: self.mainButtonStatePromise,
buttonAction: self.mainButtonActionSlot,
updateInProgress: { inProgress in
updateInProgressImpl?(inProgress)
},
updateTabBarAlpha: { alpha, transition in
updateTabBarAlphaImpl?(alpha, transition)
},
present: { c in
presentImpl?(c)
},
push: { c in
pushImpl?(c)
},
completion: { duration in
completionImpl?(duration)
}
), navigationBarAppearance: .transparent, presentationMode: .modal)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.setLeftBarButton(cancelItem, animated: false)
self.navigationPresentation = .modal
updateInProgressImpl = { [weak self] inProgress in
if let strongSelf = self {
strongSelf.navigationItem.leftBarButtonItem?.isEnabled = !inProgress
strongSelf.view.disablesInteractiveTransitionGestureRecognizer = inProgress
strongSelf.view.disablesInteractiveModalDismiss = inProgress
}
}
presentImpl = { [weak self] c in
self?.present(c, in: .window(.root))
}
pushImpl = { c in
pushController(c)
}
completionImpl = { _ in
completion()
}
updateTabBarAlphaImpl = { [weak self] alpha, transition in
self?.updateTabBarAlpha(alpha, transition)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func cancelPressed() {
self.dismiss()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
self.didSetReady = true
if let view = self.node.hostView.findTaggedView(tag: GiftAvatarComponent.View.Tag()) as? GiftAvatarComponent.View {
self._ready.set(view.ready)
} else {
self._ready.set(.single(true))
}
}
}
@objc fileprivate func mainButtonPressed() {
self.mainButtonActionSlot.invoke(Void())
}
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var mediaPickerContext: AttachmentMediaPickerContext? {
return PremiumGiftContext(controller: self)
}
}
private final class PremiumGiftContext: AttachmentMediaPickerContext {
private weak var controller: PremiumGiftScreen?
var selectionCount: Signal<Int, NoError> {
return .single(0)
}
var caption: Signal<NSAttributedString?, NoError> {
return .single(nil)
}
public var loadingProgress: Signal<CGFloat?, NoError> {
return .single(nil)
}
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
}
init(controller: PremiumGiftScreen) {
self.controller = controller
}
func setCaption(_ caption: NSAttributedString) {
}
func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) {
}
func schedule() {
}
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
}