Swiftgram/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
2023-11-02 20:29:24 +04:00

2991 lines
127 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 MultilineTextWithEntitiesComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import BlurredBackgroundComponent
import Markdown
import InAppPurchaseManager
import ConfettiEffect
import TextFormat
import InstantPageCache
import UniversalMediaPlayer
import CheckNode
import AnimationCache
import MultiAnimationRenderer
import TelegramNotices
public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
switch lhs {
case .settings:
if case .settings = rhs {
return true
} else {
return false
}
case .stickers:
if case .stickers = rhs {
return true
} else {
return false
}
case .reactions:
if case .reactions = rhs {
return true
} else {
return false
}
case .ads:
if case .ads = rhs {
return true
} else {
return false
}
case .upload:
if case .upload = rhs {
return true
} else {
return false
}
case .groupsAndChannels:
if case .groupsAndChannels = rhs {
return true
} else {
return false
}
case .pinnedChats:
if case .pinnedChats = rhs {
return true
} else {
return false
}
case .publicLinks:
if case .publicLinks = rhs {
return true
} else {
return false
}
case .savedGifs:
if case .savedGifs = rhs {
return true
} else {
return false
}
case .savedStickers:
if case .savedStickers = rhs {
return true
} else {
return false
}
case .folders:
if case .folders = rhs {
return true
} else {
return false
}
case .chatsPerFolder:
if case .chatsPerFolder = rhs {
return true
} else {
return false
}
case .accounts:
if case .accounts = rhs {
return true
} else {
return false
}
case .about:
if case .about = rhs {
return true
} else {
return false
}
case .appIcons:
if case .appIcons = rhs {
return true
} else {
return false
}
case .animatedEmoji:
if case .animatedEmoji = rhs {
return true
} else {
return false
}
case let .deeplink(link):
if case .deeplink(link) = rhs {
return true
} else {
return false
}
case let .profile(peerId):
if case .profile(peerId) = rhs {
return true
} else {
return false
}
case let .emojiStatus(lhsPeerId, lhsFileId, lhsFile, _):
if case let .emojiStatus(rhsPeerId, rhsFileId, rhsFile, _) = rhs {
return lhsPeerId == rhsPeerId && lhsFileId == rhsFileId && lhsFile == rhsFile
} else {
return false
}
case let .gift(from, to, duration):
if case .gift(from, to, duration) = rhs {
return true
} else {
return false
}
case .giftTerms:
if case .giftTerms = rhs {
return true
} else {
return false
}
case .voiceToText:
if case .voiceToText = rhs {
return true
} else {
return false
}
case .fasterDownload:
if case .fasterDownload = rhs {
return true
} else {
return false
}
case .translation:
if case .translation = rhs {
return true
} else {
return false
}
case .linksPerSharedFolder:
if case .linksPerSharedFolder = rhs {
return true
} else {
return false
}
case .membershipInSharedFolders:
if case .membershipInSharedFolders = rhs {
return true
} else {
return false
}
case .stories:
if case .stories = rhs {
return true
} else {
return false
}
case .storiesDownload:
if case .storiesDownload = rhs {
return true
} else {
return false
}
case .storiesStealthMode:
if case .storiesStealthMode = rhs {
return true
} else {
return false
}
case .storiesPermanentViews:
if case .storiesPermanentViews = rhs {
return true
} else {
return false
}
case .storiesFormatting:
if case .storiesFormatting = rhs {
return true
} else {
return false
}
case .storiesExpirationDurations:
if case .storiesExpirationDurations = rhs {
return true
} else {
return false
}
case .storiesSuggestedReactions:
if case .storiesSuggestedReactions = rhs {
return true
} else {
return false
}
case let .channelBoost(peerId):
if case .channelBoost(peerId) = rhs {
return true
} else {
return false
}
case .nameColor:
if case .nameColor = rhs {
return true
} else {
return false
}
}
}
case settings
case stickers
case reactions
case ads
case upload
case groupsAndChannels
case pinnedChats
case publicLinks
case savedGifs
case savedStickers
case folders
case chatsPerFolder
case accounts
case about
case appIcons
case animatedEmoji
case deeplink(String?)
case profile(EnginePeer.Id)
case emojiStatus(EnginePeer.Id, Int64, TelegramMediaFile?, LoadedStickerPack?)
case gift(from: EnginePeer.Id, to: EnginePeer.Id, duration: Int32)
case giftTerms
case voiceToText
case fasterDownload
case translation
case linksPerSharedFolder
case membershipInSharedFolders
case stories
case storiesDownload
case storiesStealthMode
case storiesPermanentViews
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
case channelBoost(EnginePeer.Id)
case nameColor
var identifier: String? {
switch self {
case .settings:
return "settings"
case .stickers:
return "premium_stickers"
case .reactions:
return "infinite_reactions"
case .ads:
return "no_ads"
case .upload:
return "more_upload"
case .appIcons:
return "app_icons"
case .groupsAndChannels:
return "double_limits__channels"
case .pinnedChats:
return "double_limits__dialog_pinned"
case .publicLinks:
return "double_limits__channels_public"
case .savedGifs:
return "double_limits__saved_gifs"
case .savedStickers:
return "double_limits__stickers_faved"
case .folders:
return "double_limits__dialog_filters"
case .chatsPerFolder:
return "double_limits__dialog_filters_chats"
case .accounts:
return "double_limits__accounts"
case .about:
return "double_limits__about"
case .animatedEmoji:
return "animated_emoji"
case let .profile(id):
return "profile__\(id.id._internalGetInt64Value())"
case .emojiStatus:
return "emoji_status"
case .voiceToText:
return "voice_to_text"
case .fasterDownload:
return "faster_download"
case .gift, .giftTerms:
return nil
case let .deeplink(reference):
if let reference = reference {
return "deeplink_\(reference)"
} else {
return "deeplink"
}
case .translation:
return "translations"
case .linksPerSharedFolder:
return "double_limits__community_invites"
case .membershipInSharedFolders:
return "double_limits__communities_joined"
case .stories:
return "stories"
case .storiesDownload:
return "stories__save_stories_to_gallery"
case .storiesStealthMode:
return "stories__stealth_mode"
case .storiesPermanentViews:
return "stories__permanent_views_history"
case .storiesFormatting:
return "stories__links_and_formatting"
case .storiesExpirationDurations:
return "stories__expiration_durations"
case .storiesSuggestedReactions:
return "stories__suggested_reactions"
case let .channelBoost(peerId):
return "channel_boost__\(peerId.id._internalGetInt64Value())"
case .nameColor:
return "name_color"
}
}
}
public enum PremiumPerk: CaseIterable {
case doubleLimits
case moreUpload
case fasterDownload
case voiceToText
case noAds
case uniqueReactions
case premiumStickers
case advancedChatManagement
case profileBadge
case animatedUserpics
case appIcons
case animatedEmoji
case emojiStatus
case translation
case stories
public static var allCases: [PremiumPerk] {
return [
.doubleLimits,
.moreUpload,
.fasterDownload,
.voiceToText,
.noAds,
.uniqueReactions,
.premiumStickers,
.advancedChatManagement,
.profileBadge,
.animatedUserpics,
.appIcons,
.animatedEmoji,
.emojiStatus,
.translation,
.stories
]
}
init?(identifier: String) {
for perk in PremiumPerk.allCases {
if perk.identifier == identifier {
self = perk
return
}
}
return nil
}
var identifier: String {
switch self {
case .doubleLimits:
return "double_limits"
case .moreUpload:
return "more_upload"
case .fasterDownload:
return "faster_download"
case .voiceToText:
return "voice_to_text"
case .noAds:
return "no_ads"
case .uniqueReactions:
return "infinite_reactions"
case .premiumStickers:
return "premium_stickers"
case .advancedChatManagement:
return "advanced_chat_management"
case .profileBadge:
return "profile_badge"
case .animatedUserpics:
return "animated_userpics"
case .appIcons:
return "app_icons"
case .animatedEmoji:
return "animated_emoji"
case .emojiStatus:
return "emoji_status"
case .translation:
return "translations"
case .stories:
return "stories"
}
}
func title(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
return strings.Premium_DoubledLimits
case .moreUpload:
return strings.Premium_UploadSize
case .fasterDownload:
return strings.Premium_FasterSpeed
case .voiceToText:
return strings.Premium_VoiceToText
case .noAds:
return strings.Premium_NoAds
case .uniqueReactions:
return strings.Premium_InfiniteReactions
case .premiumStickers:
return strings.Premium_Stickers
case .advancedChatManagement:
return strings.Premium_ChatManagement
case .profileBadge:
return strings.Premium_Badge
case .animatedUserpics:
return strings.Premium_Avatar
case .appIcons:
return strings.Premium_AppIcon
case .animatedEmoji:
return strings.Premium_AnimatedEmoji
case .emojiStatus:
return strings.Premium_EmojiStatus
case .translation:
return strings.Premium_Translation
case .stories:
return strings.Premium_Stories
}
}
func subtitle(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
return strings.Premium_DoubledLimitsInfo
case .moreUpload:
return strings.Premium_UploadSizeInfo
case .fasterDownload:
return strings.Premium_FasterSpeedInfo
case .voiceToText:
return strings.Premium_VoiceToTextInfo
case .noAds:
return strings.Premium_NoAdsInfo
case .uniqueReactions:
return strings.Premium_InfiniteReactionsInfo
case .premiumStickers:
return strings.Premium_StickersInfo
case .advancedChatManagement:
return strings.Premium_ChatManagementInfo
case .profileBadge:
return strings.Premium_BadgeInfo
case .animatedUserpics:
return strings.Premium_AvatarInfo
case .appIcons:
return strings.Premium_AppIconInfo
case .animatedEmoji:
return strings.Premium_AnimatedEmojiInfo
case .emojiStatus:
return strings.Premium_EmojiStatusInfo
case .translation:
return strings.Premium_TranslationInfo
case .stories:
return strings.Premium_StoriesInfo
}
}
var iconName: String {
switch self {
case .doubleLimits:
return "Premium/Perk/Limits"
case .moreUpload:
return "Premium/Perk/Upload"
case .fasterDownload:
return "Premium/Perk/Speed"
case .voiceToText:
return "Premium/Perk/Voice"
case .noAds:
return "Premium/Perk/NoAds"
case .uniqueReactions:
return "Premium/Perk/Reactions"
case .premiumStickers:
return "Premium/Perk/Stickers"
case .advancedChatManagement:
return "Premium/Perk/Chat"
case .profileBadge:
return "Premium/Perk/Badge"
case .animatedUserpics:
return "Premium/Perk/Avatar"
case .appIcons:
return "Premium/Perk/AppIcon"
case .animatedEmoji:
return "Premium/Perk/Emoji"
case .emojiStatus:
return "Premium/Perk/Status"
case .translation:
return "Premium/Perk/Translation"
case .stories:
return "Premium/Perk/Stories"
}
}
}
struct PremiumIntroConfiguration {
static var defaultValue: PremiumIntroConfiguration {
return PremiumIntroConfiguration(perks: [
.stories,
.doubleLimits,
.moreUpload,
.fasterDownload,
.translation,
.voiceToText,
.noAds,
.emojiStatus,
.uniqueReactions,
.premiumStickers,
.animatedEmoji,
.advancedChatManagement,
.profileBadge,
.animatedUserpics,
.appIcons
])
}
let perks: [PremiumPerk]
fileprivate init(perks: [PremiumPerk]) {
self.perks = perks
}
public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration {
if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] {
var perks: [PremiumPerk] = []
for value in values {
if let perk = PremiumPerk(identifier: value) {
if !perks.contains(perk) {
perks.append(perk)
} else {
perks = []
break
}
} else {
perks = []
break
}
}
if perks.count < 4 {
perks = PremiumIntroConfiguration.defaultValue.perks
}
return PremiumIntroConfiguration(perks: perks)
} else {
return .defaultValue
}
}
}
private struct PremiumProduct: Equatable {
let option: PremiumPromoConfiguration.PremiumProductOption
let storeProduct: InAppPurchaseManager.Product
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.option.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
var isCurrent: Bool {
return self.option.isCurrent
}
var transactionId: String? {
return self.option.transactionId
}
}
final class PremiumOptionComponent: CombinedComponent {
let title: String
let subtitle: String
let labelPrice: String
let discount: String
let selected: Bool
let primaryTextColor: UIColor
let secondaryTextColor: UIColor
let accentColor: UIColor
let checkForegroundColor: UIColor
let checkBorderColor: UIColor
init(
title: String,
subtitle: String,
labelPrice: String,
discount: String,
selected: Bool,
primaryTextColor: UIColor,
secondaryTextColor: UIColor,
accentColor: UIColor,
checkForegroundColor: UIColor,
checkBorderColor: UIColor
) {
self.title = title
self.subtitle = subtitle
self.labelPrice = labelPrice
self.discount = discount
self.selected = selected
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
self.accentColor = accentColor
self.checkForegroundColor = checkForegroundColor
self.checkBorderColor = checkBorderColor
}
static func ==(lhs: PremiumOptionComponent, rhs: PremiumOptionComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.labelPrice != rhs.labelPrice {
return false
}
if lhs.discount != rhs.discount {
return false
}
if lhs.selected != rhs.selected {
return false
}
if lhs.primaryTextColor != rhs.primaryTextColor {
return false
}
if lhs.secondaryTextColor != rhs.secondaryTextColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.checkForegroundColor != rhs.checkForegroundColor {
return false
}
if lhs.checkBorderColor != rhs.checkBorderColor {
return false
}
return true
}
static var body: Body {
let check = Child(CheckComponent.self)
let title = Child(MultilineTextComponent.self)
let subtitle = Child(MultilineTextComponent.self)
let discountBackground = Child(RoundedRectangle.self)
let discount = Child(MultilineTextComponent.self)
let label = Child(MultilineTextComponent.self)
return { context in
let component = context.component
var insets = UIEdgeInsets(top: 11.0, left: 46.0, bottom: 13.0, right: 16.0)
let label = label.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.labelPrice,
font: Font.regular(17),
textColor: component.secondaryTextColor
)
),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.title,
font: Font.regular(17),
textColor: component.primaryTextColor
)
),
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height),
transition: context.transition
)
var spacing: CGFloat = 0.0
var subtitleSize = CGSize()
if !component.subtitle.isEmpty {
spacing = 2.0
let subtitleFont = Font.regular(13)
let subtitleColor = component.secondaryTextColor
let subtitleString = parseMarkdownIntoAttributedString(
component.subtitle,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor),
bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]),
link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor),
linkAttribute: { _ in return nil }
)
)
let subtitle = subtitle.update(
component: MultilineTextComponent(
text: .plain(subtitleString),
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
context.add(subtitle
.position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0))
)
subtitleSize = subtitle.size
insets.top -= 2.0
insets.bottom -= 2.0
}
let discountSize: CGSize
if !component.discount.isEmpty {
let discount = discount.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.discount,
font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []),
textColor: .white
)
),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
discountSize = CGSize(width: discount.size.width + 6.0, height: 18.0)
let discountBackground = discountBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0
),
availableSize: discountSize,
transition: context.transition
)
context.add(discountBackground
.position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0))
)
context.add(discount
.position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0))
)
} else {
discountSize = CGSize(width: 0.0, height: 18.0)
}
let check = check.update(
component: CheckComponent(
theme: CheckComponent.Theme(
backgroundColor: component.accentColor,
strokeColor: component.checkForegroundColor,
borderColor: component.checkBorderColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
),
selected: component.selected
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(title
.position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0))
)
let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleSize.height + insets.bottom)
let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitleSize.width
let labelY: CGFloat
if distance > 8.0 {
labelY = size.height / 2.0
} else {
labelY = insets.top + title.size.height / 2.0
}
context.add(label
.position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelY))
)
context.add(check
.position(CGPoint(x: 4.0 + check.size.width / 2.0, y: size.height / 2.0))
)
return size
}
}
}
private final class CheckComponent: Component {
struct Theme: Equatable {
public let backgroundColor: UIColor
public let strokeColor: UIColor
public let borderColor: UIColor
public let overlayBorder: Bool
public let hasInset: Bool
public let hasShadow: Bool
public let filledBorder: Bool
public let borderWidth: CGFloat?
public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) {
self.backgroundColor = backgroundColor
self.strokeColor = strokeColor
self.borderColor = borderColor
self.overlayBorder = overlayBorder
self.hasInset = hasInset
self.hasShadow = hasShadow
self.filledBorder = filledBorder
self.borderWidth = borderWidth
}
var checkNodeTheme: CheckNodeTheme {
return CheckNodeTheme(
backgroundColor: self.backgroundColor,
strokeColor: self.strokeColor,
borderColor: self.borderColor,
overlayBorder: self.overlayBorder,
hasInset: self.hasInset,
hasShadow: self.hasShadow,
filledBorder: self.filledBorder,
borderWidth: self.borderWidth
)
}
}
let theme: Theme
let selected: Bool
init(
theme: Theme,
selected: Bool
) {
self.theme = theme
self.selected = selected
}
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.selected != rhs.selected {
return false
}
return true
}
final class View: UIView {
private var currentValue: CGFloat?
private var animator: DisplayLinkAnimator?
private var checkLayer: CheckLayer {
return self.layer as! CheckLayer
}
override class var layerClass: AnyClass {
return CheckLayer.self
}
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.checkLayer.setSelected(component.selected, animated: true)
self.checkLayer.theme = component.theme.checkNodeTheme
return CGSize(width: 22.0, height: 22.0)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class SectionGroupComponent: Component {
public final class Item: Equatable {
public let content: AnyComponentWithIdentity<Empty>
public let accessibilityLabel: String
public let isEnabled: Bool
public let action: () -> Void
public init(_ content: AnyComponentWithIdentity<Empty>, accessibilityLabel: String, isEnabled: Bool = true, action: @escaping () -> Void) {
self.content = content
self.accessibilityLabel = accessibilityLabel
self.isEnabled = isEnabled
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.accessibilityLabel != rhs.accessibilityLabel {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
return true
}
}
public let items: [Item]
public let backgroundColor: UIColor
public let selectionColor: UIColor
public let separatorColor: UIColor
public init(
items: [Item],
backgroundColor: UIColor,
selectionColor: UIColor,
separatorColor: UIColor
) {
self.items = items
self.backgroundColor = backgroundColor
self.selectionColor = selectionColor
self.separatorColor = separatorColor
}
public static func ==(lhs: SectionGroupComponent, rhs: SectionGroupComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.selectionColor != rhs.selectionColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
return true
}
public final class View: UIView {
private var buttonViews: [AnyHashable: HighlightTrackingButton] = [:]
private var itemViews: [AnyHashable: ComponentHostView<Empty>] = [:]
private var separatorViews: [UIView] = []
private var component: SectionGroupComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed(_ sender: HighlightTrackingButton) {
guard let component = self.component else {
return
}
if let (id, _) = self.buttonViews.first(where: { $0.value === sender }), let item = component.items.first(where: { $0.content.id == id }) {
item.action()
}
}
func update(component: SectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let sideInset: CGFloat = 16.0
self.backgroundColor = component.backgroundColor
var size = CGSize(width: availableSize.width, height: 0.0)
var validIds: [AnyHashable] = []
var i = 0
for item in component.items {
validIds.append(item.content.id)
let buttonView: HighlightTrackingButton
let itemView: ComponentHostView<Empty>
var itemTransition = transition
if let current = self.buttonViews[item.content.id] {
buttonView = current
} else {
buttonView = HighlightTrackingButton()
buttonView.isMultipleTouchEnabled = false
buttonView.isExclusiveTouch = true
buttonView.addTarget(self, action: #selector(self.buttonPressed(_:)), for: .touchUpInside)
self.buttonViews[item.content.id] = buttonView
self.addSubview(buttonView)
}
buttonView.accessibilityLabel = item.accessibilityLabel
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 - sideInset, height: .greatestFiniteMagnitude)
)
buttonView.isEnabled = item.isEnabled
itemView.alpha = item.isEnabled ? 1.0 : 0.3
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel))
itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset, y: itemFrame.minY + floor((itemFrame.height - itemSize.height) / 2.0)), size: itemSize)
itemView.isUserInteractionEnabled = false
buttonView.highligthedChanged = { [weak buttonView] highlighted in
if highlighted {
buttonView?.backgroundColor = component.selectionColor
} else {
UIView.animate(withDuration: 0.3, animations: {
buttonView?.backgroundColor = nil
})
}
}
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 * 2.0 + 30.0, y: itemFrame.maxY), size: CGSize(width: size.width - sideInset * 2.0 - 30.0, height: UIScreenPixel))
}
i += 1
}
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.isEmpty, 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)
}
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)
}
}
final class PerkComponent: CombinedComponent {
let iconName: String
let iconBackgroundColors: [UIColor]
let title: String
let titleColor: UIColor
let subtitle: String
let subtitleColor: UIColor
let arrowColor: UIColor
let accentColor: UIColor
let badge: String?
init(
iconName: String,
iconBackgroundColors: [UIColor],
title: String,
titleColor: UIColor,
subtitle: String,
subtitleColor: UIColor,
arrowColor: UIColor,
accentColor: UIColor,
badge: String? = nil
) {
self.iconName = iconName
self.iconBackgroundColors = iconBackgroundColors
self.title = title
self.titleColor = titleColor
self.subtitle = subtitle
self.subtitleColor = subtitleColor
self.arrowColor = arrowColor
self.accentColor = accentColor
self.badge = badge
}
static func ==(lhs: PerkComponent, rhs: PerkComponent) -> Bool {
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconBackgroundColors != rhs.iconBackgroundColors {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.subtitleColor != rhs.subtitleColor {
return false
}
if lhs.arrowColor != rhs.arrowColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
static var body: Body {
let iconBackground = Child(RoundedRectangle.self)
let icon = Child(BundleIconComponent.self)
let title = Child(MultilineTextComponent.self)
let subtitle = Child(MultilineTextComponent.self)
let arrow = Child(BundleIconComponent.self)
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let sideInset: CGFloat = 16.0
let iconTopInset: CGFloat = 15.0
let textTopInset: CGFloat = 9.0
let textBottomInset: CGFloat = 9.0
let spacing: CGFloat = 2.0
let iconSize = CGSize(width: 30.0, height: 30.0)
let iconBackground = iconBackground.update(
component: RoundedRectangle(
colors: component.iconBackgroundColors,
cornerRadius: 7.0,
gradientDirection: .vertical),
availableSize: iconSize,
transition: context.transition
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: .white
),
availableSize: iconSize,
transition: context.transition
)
let arrow = arrow.update(
component: BundleIconComponent(
name: "Item List/DisclosureArrow",
tintColor: component.arrowColor
),
availableSize: context.availableSize,
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.title,
font: Font.regular(17),
textColor: component.titleColor
)
),
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - iconBackground.size.width - sideInset * 2.83, height: context.availableSize.height),
transition: context.transition
)
let subtitle = subtitle.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.subtitle,
font: Font.regular(13),
textColor: component.subtitleColor
)
),
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - iconBackground.size.width - sideInset * 2.83, height: context.availableSize.height),
transition: context.transition
)
let iconPosition = CGPoint(x: iconBackground.size.width / 2.0, y: iconTopInset + iconBackground.size.height / 2.0)
context.add(iconBackground
.position(iconPosition)
)
context.add(icon
.position(iconPosition)
)
context.add(title
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
colors: [component.accentColor],
cornerRadius: 5.0,
gradientDirection: .vertical),
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
)
context.add(badgeText
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
)
}
context.add(subtitle
.position(CGPoint(x: iconBackground.size.width + sideInset + subtitle.size.width / 2.0, y: textTopInset + title.size.height + spacing + subtitle.size.height / 2.0))
)
let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + subtitle.size.height + textBottomInset)
context.add(arrow
.position(CGPoint(x: context.availableSize.width - 7.0 - arrow.size.width / 2.0, y: size.height / 2.0))
)
return size
}
}
}
private final class PremiumIntroScreenContentComponent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
let context: AccountContext
let source: PremiumSource
let forceDark: Bool
let isPremium: Bool?
let justBought: Bool
let otherPeerName: String?
let products: [PremiumProduct]?
let selectedProductId: String?
let validPurchases: [InAppPurchaseManager.ReceiptPurchase]
let promoConfiguration: PremiumPromoConfiguration?
let present: (ViewController) -> Void
let selectProduct: (String) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
init(
context: AccountContext,
source: PremiumSource,
forceDark: Bool,
isPremium: Bool?,
justBought: Bool,
otherPeerName: String?,
products: [PremiumProduct]?,
selectedProductId: String?,
validPurchases: [InAppPurchaseManager.ReceiptPurchase],
promoConfiguration: PremiumPromoConfiguration?,
present: @escaping (ViewController) -> Void,
selectProduct: @escaping (String) -> Void,
buy: @escaping () -> Void,
updateIsFocused: @escaping (Bool) -> Void
) {
self.context = context
self.source = source
self.forceDark = forceDark
self.isPremium = isPremium
self.justBought = justBought
self.otherPeerName = otherPeerName
self.products = products
self.selectedProductId = selectedProductId
self.validPurchases = validPurchases
self.promoConfiguration = promoConfiguration
self.present = present
self.selectProduct = selectProduct
self.buy = buy
self.updateIsFocused = updateIsFocused
}
static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.forceDark != rhs.forceDark {
return false
}
if lhs.justBought != rhs.justBought {
return false
}
if lhs.otherPeerName != rhs.otherPeerName {
return false
}
if lhs.products != rhs.products {
return false
}
if lhs.selectedProductId != rhs.selectedProductId {
return false
}
if lhs.validPurchases != rhs.validPurchases {
return false
}
if lhs.promoConfiguration != rhs.promoConfiguration {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
var products: [PremiumProduct]?
var selectedProductId: String?
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var newPerks: [String] = []
var isPremium: Bool?
private var disposable: Disposable?
private(set) var configuration = PremiumIntroConfiguration.defaultValue
private var stickersDisposable: Disposable?
private var newPerksDisposable: Disposable?
private var preloadDisposableSet = DisposableSet()
var price: String? {
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
}
var isAnnual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
}
} else {
return false
}
}
init(context: AccountContext, source: PremiumSource) {
self.context = context
super.init()
self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration in
if let strongSelf = self {
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
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.promo_screen_show", data: json)
}
}
}
})
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())
}
}
}
}
})
self.newPerksDisposable = (ApplicationSpecificNotice.dismissedPremiumAppIconsBadge(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge in
guard let self else {
return
}
let newPerks: [String] = []
self.newPerks = newPerks
self.updated()
})
}
deinit {
self.disposable?.dispose()
self.preloadDisposableSet.dispose()
self.stickersDisposable?.dispose()
self.newPerksDisposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, source: self.source)
}
static var body: Body {
let overscroll = Child(Rectangle.self)
let fade = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let optionsSection = Child(SectionGroupComponent.self)
let perksSection = Child(SectionGroupComponent.self)
let infoBackground = Child(RoundedRectangle.self)
let infoTitle = Child(MultilineTextComponent.self)
let infoText = Child(MultilineTextComponent.self)
let termsText = 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 state = context.state
state.products = context.component.products
state.selectedProductId = context.component.selectedProductId
state.validPurchases = context.component.validPurchases
state.isPremium = context.component.isPremium
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)
var topBackgroundColor = theme.list.plainBackgroundColor
let bottomBackgroundColor = theme.list.blocksBackgroundColor
if theme.overallDarkAppearance {
topBackgroundColor = bottomBackgroundColor
}
let overscroll = overscroll.update(
component: Rectangle(color: topBackgroundColor),
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))
)
let fade = fade.update(
component: RoundedRectangle(
colors: [
topBackgroundColor,
bottomBackgroundColor
],
cornerRadius: 0.0,
gradientDirection: .vertical
),
availableSize: CGSize(width: availableWidth, height: 300),
transition: context.transition
)
context.add(fade
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
)
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
let textColor = theme.list.itemPrimaryTextColor
let accentColor = theme.list.itemAccentColor
let titleColor = theme.list.itemPrimaryTextColor
let subtitleColor = theme.list.itemSecondaryTextColor
let arrowColor = theme.list.disclosureArrowColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textString: String
if case .emojiStatus = context.component.source {
textString = strings.Premium_EmojiStatusText.replacingOccurrences(of: "#", with: "# ")
} else if case .giftTerms = context.component.source {
textString = strings.Premium_PersonalDescription
} else if let _ = context.component.otherPeerName {
if case let .gift(fromId, _, _) = context.component.source {
if fromId == context.component.context.account.peerId {
textString = strings.Premium_GiftedDescriptionYou
} else {
textString = strings.Premium_GiftedDescription
}
} else {
textString = strings.Premium_PersonalDescription
}
} else if context.component.isPremium == true {
if !context.component.justBought, let products = state.products, let current = products.first(where: { $0.isCurrent }), current.months == 1 {
textString = strings.Premium_UpgradeDescription
} else {
textString = strings.Premium_SubscribedDescription
}
} else {
textString = strings.Premium_Description
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: textString,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - 8.0, height: 240.0),
transition: .immediate
)
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
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)
]
let accountContext = context.component.context
let present = context.component.present
let selectProduct = context.component.selectProduct
let buy = context.component.buy
let updateIsFocused = context.component.updateIsFocused
let layoutOptions = {
if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) {
var optionsItems: [SectionGroupComponent.Item] = []
let gradientColors: [UIColor] = [
UIColor(rgb: 0x8e77ff),
UIColor(rgb: 0x9a6fff),
UIColor(rgb: 0xb36eee)
]
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount)), product.storeProduct.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
let currentProductMonths = state.products?.first(where: { $0.isCurrent })?.months ?? 0
var i = 0
for product in products {
let giftTitle: String
if product.id.hasSuffix(".monthly") {
giftTitle = strings.Premium_Monthly
} else if product.id.hasSuffix(".semiannual") {
giftTitle = strings.Premium_Semiannual
} else {
giftTitle = strings.Premium_Annual
}
let fraction = Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.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.price
if product.months > 1 {
pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months))
if discountValue > 0 {
subtitle = "**\(defaultPrice)** \(product.price)"
accessibilitySubtitle = product.price
if product.months == 12 {
subtitle = environment.strings.Premium_PricePerYear(subtitle).string
accessibilitySubtitle = environment.strings.Premium_PricePerYear(accessibilitySubtitle).string
}
} else {
subtitle = product.price
}
}
if product.isCurrent {
subtitle = environment.strings.Premium_CurrentPlan
accessibilitySubtitle = subtitle
}
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
optionsItems.append(
SectionGroupComponent.Item(
AnyComponentWithIdentity(
id: product.id,
component: AnyComponent(
PremiumOptionComponent(
title: giftTitle,
subtitle: subtitle,
labelPrice: pricePerMonth,
discount: discount,
selected: !product.isCurrent && product.id == state.selectedProductId,
primaryTextColor: textColor,
secondaryTextColor: subtitleColor,
accentColor: gradientColors[i],
checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor,
checkBorderColor: environment.theme.list.itemCheckColors.strokeColor
)
)
),
accessibilityLabel: "\(giftTitle). \(accessibilitySubtitle). \(pricePerMonth)",
isEnabled: product.months > currentProductMonths,
action: {
selectProduct(product.id)
}
)
)
i += 1
}
let optionsSection = optionsSection.update(
component: SectionGroupComponent(
items: optionsItems,
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
if case .emojiStatus = context.component.source {
size.height -= 18.0
} else {
size.height += 26.0
}
}
}
let forceDark = context.component.forceDark
let layoutPerks = {
var 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,
badge: state.newPerks.contains(perk.identifier) ? strings.Premium_New : nil
)
)
),
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
// let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.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
if case .emojiStatus = context.component.source {
if state.isPremium == true {
size.height -= 23.0
} else {
size.height += 23.0
}
} else {
size.height += 23.0
}
}
if case .emojiStatus = context.component.source {
layoutPerks()
layoutOptions()
} else {
layoutOptions()
layoutPerks()
let textSideInset: CGFloat = 16.0
let textPadding: CGFloat = 13.0
let infoTitle = infoTitle.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(string: strings.Premium_AboutTitle.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor)
),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(infoTitle
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoTitle.size.width / 2.0, y: size.height + infoTitle.size.height / 2.0))
)
size.height += infoTitle.size.height
size.height += 3.0
let infoText = infoText.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_AboutText,
attributes: markdownAttributes
),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
let infoBackground = infoBackground.update(
component: RoundedRectangle(
color: environment.theme.list.itemBlocksBackgroundColor,
cornerRadius: 10.0
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: infoText.size.height + textPadding * 2.0),
transition: context.transition
)
context.add(infoBackground
.position(CGPoint(x: size.width / 2.0, y: size.height + infoBackground.size.height / 2.0))
)
context.add(infoText
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoText.size.width / 2.0, y: size.height + textPadding + infoText.size.height / 2.0))
)
size.height += infoBackground.size.height
size.height += 6.0
let termsFont = Font.regular(13.0)
let boldTermsFont = Font.semibold(13.0)
let italicTermsFont = Font.italic(13.0)
let boldItalicTermsFont = Font.semiboldItalic(13.0)
let monospaceTermsFont = Font.monospace(13.0)
let termsTextColor = environment.theme.list.freeTextColor
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
var isGiftView = false
if case let .gift(fromId, _, _) = context.component.source {
if fromId == context.component.context.account.peerId {
isGiftView = true
}
}
let termsString: MultilineTextComponent.TextContent
if isGiftView {
termsString = .plain(NSAttributedString())
} else if let promoConfiguration = context.component.promoConfiguration {
let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont, message: nil)
termsString = .plain(attributedString)
} else {
termsString = .markdown(
text: strings.Premium_Terms,
attributes: termsMarkdownAttributes
)
}
let controller = environment.controller
let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
controller.context.sharedContext.applicationBindings.openSubscriptions()
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
} else {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil, progress: nil)
})
}
}
}
}
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
termsTapActionImpl(attributes)
}
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(termsText
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0))
)
size.height += termsText.size.height
size.height += 10.0
}
size.height += scrollEnvironment.insets.bottom
if context.component.source != .settings {
size.height += 44.0
}
return size
}
}
}
private final class PremiumIntroScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let source: PremiumSource
let forceDark: Bool
let forceHasPremium: Bool
let updateInProgress: (Bool) -> Void
let present: (ViewController) -> Void
let push: (ViewController) -> Void
let completion: () -> Void
init(context: AccountContext, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
self.source = source
self.forceDark = forceDark
self.forceHasPremium = forceHasPremium
self.updateInProgress = updateInProgress
self.present = present
self.push = push
self.completion = completion
}
static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.forceDark != rhs.forceDark {
return false
}
if lhs.forceHasPremium != rhs.forceHasPremium {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let updateInProgress: (Bool) -> Void
private let present: (ViewController) -> Void
private let completion: () -> Void
var topContentOffset: CGFloat?
var bottomContentOffset: CGFloat?
var hasIdleAnimations = true
var inProgress = false
private(set) var promoConfiguration: PremiumPromoConfiguration?
private(set) var products: [PremiumProduct]?
private(set) var selectedProductId: String?
fileprivate var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var isPremium: Bool?
var otherPeerName: String?
var justBought = false
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
var emojiFile: TelegramMediaFile?
var emojiPackTitle: String?
private var emojiFileDisposable: Disposable?
private var disposable: Disposable?
private var paymentDisposable = MetaDisposable()
private var activationDisposable = MetaDisposable()
private var preloadDisposableSet = DisposableSet()
var price: String? {
return self.products?.first(where: { $0.id == self.selectedProductId })?.price
}
var isAnnual: Bool {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
}
} else {
return false
}
}
init(context: AccountContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
self.updateInProgress = updateInProgress
self.present = present
self.completion = completion
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
super.init()
self.validPurchases = context.inAppPurchaseManager?.getReceiptPurchases() ?? []
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts
} else {
availableProducts = .single([])
}
let otherPeerName: Signal<String?, NoError>
if case let .gift(fromPeerId, toPeerId, _) = source {
let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId
otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId))
|> map { peer -> String? in
return peer?.compactDisplayTitle
}
} else if case let .profile(peerId) = source {
otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> map { peer -> String? in
return peer?.compactDisplayTitle
}
} else if case let .emojiStatus(peerId, _, _, _) = source {
otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> map { peer -> String? in
return peer?.compactDisplayTitle
}
} else {
otherPeerName = .single(nil)
}
if forceHasPremium {
self.isPremium = true
}
self.disposable = combineLatest(
queue: Queue.mainQueue(),
availableProducts,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
return peer?.isPremium ?? false
},
otherPeerName
).start(next: { [weak self] availableProducts, promoConfiguration, isPremium, otherPeerName in
if let strongSelf = self {
strongSelf.promoConfiguration = promoConfiguration
let hadProducts = strongSelf.products != nil
var products: [PremiumProduct] = []
for option in promoConfiguration.premiumProductOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), product.isSubscription {
products.append(PremiumProduct(option: option, storeProduct: product))
}
}
strongSelf.products = products
strongSelf.isPremium = forceHasPremium || isPremium
strongSelf.otherPeerName = otherPeerName
if !hadProducts {
strongSelf.selectedProductId = strongSelf.products?.first?.id
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())
}
}
strongSelf.updated(transition: .immediate)
}
})
if case let .emojiStatus(_, emojiFileId, emojiFile, maybeEmojiPack) = source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack {
if let emojiFile = emojiFile {
self.emojiFile = emojiFile
self.emojiPackTitle = info.title
self.updated(transition: .immediate)
} else {
self.emojiFileDisposable = (context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId])
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.emojiFile = result[emojiFileId]
strongSelf.updated(transition: .immediate)
})
}
}
}
deinit {
self.disposable?.dispose()
self.paymentDisposable.dispose()
self.activationDisposable.dispose()
self.emojiFileDisposable?.dispose()
self.preloadDisposableSet.dispose()
}
func buy() {
guard let inAppPurchaseManager = self.context.inAppPurchaseManager,
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
var hasActiveSubsciption = false
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] {
} else if !self.validPurchases.isEmpty && !isUpgrade {
let now = Date()
for purchase in self.validPurchases.reversed() {
if (purchase.productId.hasSuffix(".monthly") || purchase.productId.hasSuffix(".annual")) && purchase.expirationDate > now {
hasActiveSubsciption = true
}
}
}
if hasActiveSubsciption {
let errorText = presentationData.strings.Premium_Purchase_OnlyOneSubscriptionAllowed
let alertController = textAlertController(context: self.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
return
}
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|> castError(AssignAppStoreTransactionError.self)
|> take(until: { view in
if let peer = view.peers[view.peerId], peer.isPremium {
return SignalTakeAction(passthrough: false, complete: true)
} else {
return SignalTakeAction(passthrough: false, complete: false)
}
})
|> mapToSignal { _ -> Signal<Never, AssignAppStoreTransactionError> in
return .never()
}
|> timeout(15.0, queue: Queue.mainQueue(), alternate: .fail(.timeout))
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
}
}, completed: { [weak self] in
if let strongSelf = self {
let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start()
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.isPremium = true
strongSelf.justBought = true
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion()
}
}))
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.inProgress = false
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
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.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 selectProduct(_ productId: String) {
self.selectedProductId = productId
self.updated(transition: .immediate)
}
}
func makeState() -> State {
return State(context: self.context, source: self.source, forceHasPremium: self.forceHasPremium, 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(PremiumStarComponent.self)
let emoji = Child(EmojiHeaderComponent.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let secondaryTitle = Child(MultilineTextWithEntitiesComponent.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
}
var isIntro = true
if case .profile = context.component.source {
isIntro = false
}
let header: _UpdatedChildComponent
if case let .emojiStatus(_, fileId, _, _) = context.component.source {
header = emoji.update(
component: EmojiHeaderComponent(
context: context.component.context,
animationCache: state.animationCache,
animationRenderer: state.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
accentColor: environment.theme.list.itemAccentColor,
fileId: fileId,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
} else {
header = star.update(
component: PremiumStarComponent(
isIntro: isIntro,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
}
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 titleString: String
if case .emojiStatus = context.component.source {
titleString = environment.strings.Premium_Title
} else if case .giftTerms = context.component.source {
titleString = environment.strings.Premium_Title
} else if case .gift = context.component.source {
titleString = environment.strings.Premium_GiftedTitle
} else if state.isPremium == true {
if !state.justBought && state.canUpgrade {
titleString = environment.strings.Premium_Title
} else {
titleString = environment.strings.Premium_SubscribedTitle
}
} else {
titleString = environment.strings.Premium_Title
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: titleString, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
var loadedEmojiPack: LoadedStickerPack?
var highlightableLinks = false
let secondaryTitleText: String
var isAnonymous = false
if var otherPeerName = state.otherPeerName {
if case let .emojiStatus(_, _, file, maybeEmojiPack) = context.component.source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack {
loadedEmojiPack = maybeEmojiPack
highlightableLinks = true
var packReference: StickerPackReference?
if let file = file {
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, reference) = attribute {
packReference = reference
}
}
}
if let packReference = packReference, case let .id(id, _) = packReference, id == 773947703670341676 {
secondaryTitleText = environment.strings.Premium_EmojiStatusShortTitle(otherPeerName).string
} else {
secondaryTitleText = environment.strings.Premium_EmojiStatusTitle(otherPeerName, info.title).string.replacingOccurrences(of: "#", with: " # ")
}
} else if case .profile = context.component.source {
secondaryTitleText = environment.strings.Premium_PersonalTitle(otherPeerName).string
} else if case let .gift(fromPeerId, _, duration) = context.component.source {
if fromPeerId == context.component.context.account.peerId {
if duration == 12 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_12Month(otherPeerName).string
} else if duration == 6 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_6Month(otherPeerName).string
} else if duration == 3 {
secondaryTitleText = environment.strings.Premium_GiftedTitleYou_3Month(otherPeerName).string
} else {
secondaryTitleText = ""
}
} else {
if fromPeerId.namespace == Namespaces.Peer.CloudUser && fromPeerId.id._internalGetInt64Value() == 777000 {
isAnonymous = true
otherPeerName = environment.strings.Premium_GiftedTitle_Someone
}
if duration == 12 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_12Month(otherPeerName).string
} else if duration == 6 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_6Month(otherPeerName).string
} else if duration == 3 {
secondaryTitleText = environment.strings.Premium_GiftedTitle_3Month(otherPeerName).string
} else {
secondaryTitleText = ""
}
}
} else {
secondaryTitleText = ""
}
} else {
secondaryTitleText = ""
}
let textColor = environment.theme.list.itemPrimaryTextColor
let accentColor: UIColor
if case .emojiStatus = context.component.source {
accentColor = environment.theme.list.itemAccentColor
} else {
accentColor = UIColor(rgb: 0x597cf5)
}
let textFont = Font.bold(18.0)
let boldTextFont = Font.bold(18.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: isAnonymous ? textColor : accentColor), linkAttribute: { _ in
return nil
})
let secondaryAttributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryTitleText, attributes: markdownAttributes))
if let emojiFile = state.emojiFile {
let range = (secondaryAttributedText.string as NSString).range(of: "#")
if range.location != NSNotFound {
secondaryAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range)
}
}
let accountContext = context.component.context
let presentController = context.component.present
let secondaryTitle = secondaryTitle.update(
component: MultilineTextWithEntitiesComponent(
context: context.component.context,
animationCache: context.state.animationCache,
animationRenderer: context.state.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
text: .plain(secondaryAttributedText),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 2,
lineSpacing: 0.0,
highlightAction: highlightableLinks ? { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
} : nil,
tapAction: { [weak state, weak environment] _, _ in
if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
for attribute in emojiFile.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference {
let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedEmojiPack.flatMap { [$0] } ?? [], parentNavigationController: navigationController, sendSticker: { _, _, _ in
return false
})
presentController(controller)
break
}
}
}
}
),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.width),
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 = state.isPremium == true && !state.canUpgrade ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumIntroScreenContentComponent(
context: context.component.context,
source: context.component.source,
forceDark: context.component.forceDark,
isPremium: state.isPremium,
justBought: state.justBought,
otherPeerName: state.otherPeerName,
products: state.products,
selectedProductId: state.selectedProductId,
validPurchases: state.validPurchases,
promoConfiguration: state.promoConfiguration,
present: context.component.present,
selectProduct: { [weak state] productId in
state?.selectProduct(productId)
},
buy: { [weak state] in
state?.buy()
},
updateIsFocused: { [weak state] isFocused in
state?.updateIsFocused(isFocused)
}
)),
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
)
let topInset: CGFloat = environment.navigationHeight - 56.0
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))
)
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
if state.otherPeerName != nil {
titleAlpha = min(1.0, fraction * 1.1)
} else {
titleAlpha = 1.0
}
} else {
topPanelAlpha = 0.0
titleScale = 1.0
titleOffset = 0.0
titleAlpha = state.otherPeerName != nil ? 0.0 : 1.0
}
context.add(header
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + header.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))
)
var isGiftView = false
if case let .gift(fromId, _, _) = context.component.source {
if fromId == context.component.context.account.peerId {
isGiftView = true
}
}
if (state.isPremium == true && (!state.canUpgrade || state.justBought)) || isGiftView {
} else {
let buttonTitle: String
if state.isPremium == true && state.canUpgrade {
buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "").string : environment.strings.Premium_UpgradeFor(state.price ?? "").string
} else {
buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "").string : environment.strings.Premium_SubscribeFor(state.price ?? "").string
}
let sideInset: CGFloat = 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: buttonTitle,
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
)
let bottomPanelAlpha: CGFloat
if let bottomContentOffset = state.bottomContentOffset {
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
} else {
bottomPanelAlpha = 1.0
}
context.add(bottomPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
.opacity(bottomPanelAlpha)
.disappear(Transition.Disappear { view, transition, completion in
if case .none = transition.animation {
completion()
return
}
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
})
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
.disappear(Transition.Disappear { view, transition, completion in
if case .none = transition.animation {
completion()
return
}
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
})
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
.disappear(Transition.Disappear { view, transition, completion in
if case .none = transition.animation {
completion()
return
}
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: bottomPanel.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
})
)
}
return context.availableSize
}
}
}
public final class PremiumIntroScreen: ViewControllerComponentContainer {
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?
public init(context: AccountContext, modal: Bool = true, source: PremiumSource, forceDark: Bool = false, forceHasPremium: Bool = false) {
self.context = context
var updateInProgressImpl: ((Bool) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var completionImpl: (() -> Void)?
super.init(context: context, component: PremiumIntroScreenComponent(
context: context,
source: source,
forceDark: forceDark,
forceHasPremium: forceHasPremium,
updateInProgress: { inProgress in
updateInProgressImpl?(inProgress)
},
present: { c in
presentImpl?(c)
},
push: { c in
pushImpl?(c)
},
completion: {
completionImpl?()
}
), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if modal {
let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.setLeftBarButton(cancelItem, animated: false)
self.navigationPresentation = .modal
} else {
self.navigationPresentation = .modalInLargeLayout
}
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 = { [weak self] c in
self?.push(c)
}
completionImpl = { [weak self] in
if let self {
self.animateSuccess()
}
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func cancelPressed() {
self.dismiss()
self.wasDismissed?()
}
public func animateSuccess() {
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
if let sourceView = self.sourceView {
view.animateFrom = sourceView
view.containerView = self.containerView
view.animationColor = self.animationColor
self.sourceView = nil
self.containerView = nil
self.animationColor = nil
}
} else if let view = self.node.hostView.findTaggedView(tag: EmojiHeaderComponent.View.Tag()) as? EmojiHeaderComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
if let sourceView = self.sourceView {
view.animateFrom = sourceView
view.containerView = self.containerView
view.animateIn()
self.sourceView = nil
self.containerView = nil
self.animationColor = nil
}
}
}
}
}