Swiftgram/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
Ilya Laktyushin 06d5377bd4 Update API
2024-04-04 02:41:40 +04:00

3845 lines
172 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
import UndoUI
import TelegramStringFormatting
import ListSectionComponent
import ListActionItemComponent
import EmojiStatusSelectionComponent
import EmojiStatusComponent
import EntityKeyboard
import EmojiActionIconComponent
import ScrollComponent
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, slug):
if case .gift(from, to, duration, slug) = 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 .storiesHigherQuality:
if case .storiesHigherQuality = 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 .similarChannels:
if case .similarChannels = rhs {
return true
} else {
return false
}
case .wallpapers:
if case .wallpapers = rhs {
return true
} else {
return false
}
case .presence:
if case .presence = rhs {
return true
} else {
return false
}
case .readTime:
if case .readTime = rhs {
return true
} else {
return false
}
case .messageTags:
if case .messageTags = rhs {
return true
} else {
return false
}
case .folderTags:
if case .folderTags = 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, giftCode: PremiumGiftCodeInfo?)
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 storiesHigherQuality
case channelBoost(EnginePeer.Id)
case nameColor
case similarChannels
case wallpapers
case presence
case readTime
case messageTags
case folderTags
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 .storiesHigherQuality:
return "stories__quality"
case let .channelBoost(peerId):
return "channel_boost__\(peerId.id._internalGetInt64Value())"
case .nameColor:
return "name_color"
case .similarChannels:
return "similar_channels"
case .wallpapers:
return "wallpapers"
case .presence:
return "presence"
case .readTime:
return "read_time"
case .messageTags:
return "saved_tags"
case .folderTags:
return "folder_tags"
}
}
}
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
case colors
case wallpapers
case messageTags
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
case businessIntro
case businessLinks
public static var allCases: [PremiumPerk] {
return [
.doubleLimits,
.moreUpload,
.fasterDownload,
.voiceToText,
.noAds,
.uniqueReactions,
.premiumStickers,
.advancedChatManagement,
.profileBadge,
.animatedUserpics,
.appIcons,
.animatedEmoji,
.emojiStatus,
.translation,
.stories,
.colors,
.wallpapers,
.messageTags,
.lastSeen,
.messagePrivacy,
.folderTags,
.business
]
}
public static var allBusinessCases: [PremiumPerk] {
return [
.businessLocation,
.businessHours,
.businessQuickReplies,
.businessGreetingMessage,
.businessLinks,
.businessAwayMessage,
.businessIntro,
.businessChatBots
]
}
init?(identifier: String, business: Bool) {
for perk in business ? PremiumPerk.allBusinessCases : 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"
case .colors:
return "peer_colors"
case .wallpapers:
return "wallpapers"
case .messageTags:
return "saved_tags"
case .lastSeen:
return "last_seen"
case .messagePrivacy:
return "message_privacy"
case .folderTags:
return "folder_tags"
case .business:
return "business"
case .businessLocation:
return "business_location"
case .businessHours:
return "business_hours"
case .businessQuickReplies:
return "quick_replies"
case .businessGreetingMessage:
return "greeting_message"
case .businessAwayMessage:
return "away_message"
case .businessChatBots:
return "business_bots"
case .businessIntro:
return "business_intro"
case .businessLinks:
return "business_links"
}
}
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
case .colors:
return strings.Premium_Colors
case .wallpapers:
return strings.Premium_Wallpapers
case .messageTags:
return strings.Premium_MessageTags
case .lastSeen:
return strings.Premium_LastSeen
case .messagePrivacy:
return strings.Premium_MessagePrivacy
case .folderTags:
return strings.Premium_FolderTags
case .business:
return strings.Premium_Business
case .businessLocation:
return strings.Business_Location
case .businessHours:
return strings.Business_OpeningHours
case .businessQuickReplies:
return strings.Business_QuickReplies
case .businessGreetingMessage:
return strings.Business_GreetingMessages
case .businessAwayMessage:
return strings.Business_AwayMessages
case .businessChatBots:
return strings.Business_ChatbotsItem
case .businessIntro:
return strings.Business_Intro
case .businessLinks:
return strings.Business_Links
}
}
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
case .colors:
return strings.Premium_ColorsInfo
case .wallpapers:
return strings.Premium_WallpapersInfo
case .messageTags:
return strings.Premium_MessageTagsInfo
case .lastSeen:
return strings.Premium_LastSeenInfo
case .messagePrivacy:
return strings.Premium_MessagePrivacyInfo
case .folderTags:
return strings.Premium_FolderTagsInfo
case .business:
return strings.Premium_BusinessInfo
case .businessLocation:
return strings.Business_LocationInfo
case .businessHours:
return strings.Business_OpeningHoursInfo
case .businessQuickReplies:
return strings.Business_QuickRepliesInfo
case .businessGreetingMessage:
return strings.Business_GreetingMessagesInfo
case .businessAwayMessage:
return strings.Business_AwayMessagesInfo
case .businessChatBots:
return strings.Business_ChatbotsInfo
case .businessIntro:
return strings.Business_IntroInfo
case .businessLinks:
return strings.Business_LinksInfo
}
}
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"
case .colors:
return "Premium/Perk/Colors"
case .wallpapers:
return "Premium/Perk/Wallpapers"
case .messageTags:
return "Premium/Perk/MessageTags"
case .lastSeen:
return "Premium/Perk/LastSeen"
case .messagePrivacy:
return "Premium/Perk/MessagePrivacy"
case .folderTags:
return "Premium/Perk/MessageTags"
case .business:
return "Premium/Perk/Business"
case .businessLocation:
return "Premium/BusinessPerk/Location"
case .businessHours:
return "Premium/BusinessPerk/Hours"
case .businessQuickReplies:
return "Premium/BusinessPerk/Replies"
case .businessGreetingMessage:
return "Premium/BusinessPerk/Greetings"
case .businessAwayMessage:
return "Premium/BusinessPerk/Away"
case .businessChatBots:
return "Premium/BusinessPerk/Chatbots"
case .businessIntro:
return "Premium/BusinessPerk/Intro"
case .businessLinks:
return "Premium/BusinessPerk/Links"
}
}
}
struct PremiumIntroConfiguration {
static var defaultValue: PremiumIntroConfiguration {
return PremiumIntroConfiguration(perks: [
.stories,
.moreUpload,
.doubleLimits,
.lastSeen,
.voiceToText,
.fasterDownload,
.translation,
.animatedEmoji,
.emojiStatus,
.messageTags,
.colors,
.wallpapers,
.profileBadge,
.messagePrivacy,
.advancedChatManagement,
.noAds,
.appIcons,
.uniqueReactions,
.animatedUserpics,
.premiumStickers,
.business
], businessPerks: [
.businessLocation,
.businessHours,
.businessQuickReplies,
.businessGreetingMessage,
.businessAwayMessage,
.businessLinks,
.businessIntro,
.businessChatBots
])
}
let perks: [PremiumPerk]
let businessPerks: [PremiumPerk]
fileprivate init(perks: [PremiumPerk], businessPerks: [PremiumPerk]) {
self.perks = perks
self.businessPerks = businessPerks
}
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, business: false) {
if !perks.contains(perk) {
perks.append(perk)
} else {
perks = []
break
}
} else {
perks = []
break
}
}
if perks.count < 4 {
perks = PremiumIntroConfiguration.defaultValue.perks
}
var businessPerks: [PremiumPerk] = []
if let values = data["business_promo_order"] as? [String] {
for value in values {
if let perk = PremiumPerk(identifier: value, business: true) {
if !businessPerks.contains(perk) {
businessPerks.append(perk)
} else {
businessPerks = []
break
}
}
}
}
if businessPerks.count < 4 {
businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks
}
return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks)
} 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 PerkIconComponent: CombinedComponent {
let backgroundColor: UIColor
let foregroundColor: UIColor
let iconName: String
init(
backgroundColor: UIColor,
foregroundColor: UIColor,
iconName: String
) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.iconName = iconName
}
static func ==(lhs: PerkIconComponent, rhs: PerkIconComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let iconSize = CGSize(width: 30.0, height: 30.0)
let background = background.update(
component: RoundedRectangle(
color: component.backgroundColor,
cornerRadius: 7.0
),
availableSize: iconSize,
transition: context.transition
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: .white
),
availableSize: iconSize,
transition: context.transition
)
let iconPosition = CGPoint(x: background.size.width / 2.0, y: background.size.height / 2.0)
context.add(background
.position(iconPosition)
)
context.add(icon
.position(iconPosition)
)
return iconSize
}
}
}
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.iconBackgroundColors,
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 mode: PremiumIntroScreen.Mode
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 push: (ViewController) -> Void
let selectProduct: (String) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
init(
context: AccountContext,
mode: PremiumIntroScreen.Mode,
source: PremiumSource,
forceDark: Bool,
isPremium: Bool?,
justBought: Bool,
otherPeerName: String?,
products: [PremiumProduct]?,
selectedProductId: String?,
validPurchases: [InAppPurchaseManager.ReceiptPurchase],
promoConfiguration: PremiumPromoConfiguration?,
present: @escaping (ViewController) -> Void,
push: @escaping (ViewController) -> Void,
selectProduct: @escaping (String) -> Void,
buy: @escaping () -> Void,
updateIsFocused: @escaping (Bool) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void
) {
self.context = context
self.mode = mode
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.push = push
self.selectProduct = selectProduct
self.buy = buy
self.updateIsFocused = updateIsFocused
self.copyLink = copyLink
self.shareLink = shareLink
}
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
private let present: (ViewController) -> Void
var products: [PremiumProduct]?
var selectedProductId: String?
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var newPerks: [String] = []
var isPremium: Bool?
var peer: EnginePeer?
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,
present: @escaping (ViewController) -> Void
) {
self.context = context
self.present = present
super.init()
self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.App(),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration, accountPeer in
if let strongSelf = self {
let isFirstTime = strongSelf.peer == nil
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
strongSelf.peer = accountPeer
strongSelf.updated(transition: .immediate)
if let identifier = source.identifier, isFirstTime {
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 = combineLatest(queue: Queue.mainQueue(),
ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager)
).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in
guard let self else {
return
}
var newPerks: [String] = []
if !dismissedBusinessBadge {
newPerks.append(PremiumPerk.business.identifier)
}
if !dismissedBusinessLinksBadge {
newPerks.append(PremiumPerk.businessLinks.identifier)
}
if !dismissedBusinessIntroBadge {
newPerks.append(PremiumPerk.businessIntro.identifier)
}
if !dismissedBusinessChatbotsBadge {
newPerks.append(PremiumPerk.businessChatBots.identifier)
}
self.newPerks = newPerks
self.updated()
})
}
deinit {
self.disposable?.dispose()
self.preloadDisposableSet.dispose()
self.stickersDisposable?.dispose()
self.newPerksDisposable?.dispose()
}
private var updatedPeerStatus: PeerEmojiStatus?
private weak var emojiStatusSelectionController: ViewController?
private var previousEmojiSetupTimestamp: Double?
func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) {
let currentTimestamp = CACurrentMediaTime()
if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 {
return
}
self.previousEmojiSetupTimestamp = currentTimestamp
self.emojiStatusSelectionController?.dismiss()
var selectedItems = Set<MediaId>()
if let currentFileId {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId))
}
let controller = EmojiStatusSelectionController(
context: self.context,
mode: .statusSelection,
sourceView: sourceView,
emojiContent: EmojiPagerContentComponent.emojiInputData(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
isStandalone: false,
subject: .status,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId,
selectedItems: selectedItems,
topStatusTitle: nil,
backgroundIconColor: color
),
currentSelection: currentFileId,
color: color,
destinationItemView: { [weak sourceView] in
guard let sourceView else {
return nil
}
return sourceView
}
)
self.emojiStatusSelectionController = controller
self.present(controller)
}
}
func makeState() -> State {
return State(context: self.context, source: self.source, present: self.present)
}
static var body: Body {
let overscroll = Child(Rectangle.self)
let fade = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let completedText = Child(MultilineTextComponent.self)
let linkButton = Child(Button.self)
let optionsSection = Child(SectionGroupComponent.self)
let businessSection = Child(ListSectionComponent.self)
let moreBusinessSection = Child(ListSectionComponent.self)
let adsSettingsSection = Child(ListSectionComponent.self)
let perksSection = Child(ListSectionComponent.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 presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
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 subtitleColor = theme.list.itemSecondaryTextColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
var link = ""
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, _, _, giftCode) = context.component.source {
if fromId == context.component.context.account.peerId {
textString = strings.Premium_GiftedDescriptionYou
} else {
if let giftCode {
if let _ = giftCode.usedDate {
textString = strings.Premium_Gift_UsedLink_Text
} else {
link = "https://t.me/giftcode/\(giftCode.slug)"
textString = strings.Premium_Gift_Link_Text
}
} else {
textString = strings.Premium_GiftedDescription
}
}
} else {
textString = strings.Premium_PersonalDescription
}
} else if context.component.isPremium == true {
if case .business = context.component.mode {
textString = strings.Business_SubscribedDescription
} else {
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 {
if case .business = context.component.mode {
textString = strings.Business_Description
} 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: { contents in
return (TelegramTextAttributes.URL, contents)
})
let shareLink = context.component.shareLink
let textComponent: _ConcreteChildComponent<MultilineTextComponent>
if context.component.justBought {
textComponent = completedText
} else {
textComponent = text
}
let text = textComponent.update(
component: MultilineTextComponent(
text: .markdown(
text: textString,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
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: { _, _ in
shareLink(link)
}
),
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))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
size.height += text.size.height
size.height += 21.0
let gradientColors: [UIColor] = [
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe95a2c),
UIColor(rgb: 0xe74e33),
UIColor(rgb: 0xe74e33), //replace
UIColor(rgb: 0xe54937),
UIColor(rgb: 0xe3433c),
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xcb3e6d),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xa34cd7),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff),
UIColor(rgb: 0x676bff),
UIColor(rgb: 0x676bff), //replace
UIColor(rgb: 0x6172ff),
UIColor(rgb: 0x5b79ff),
UIColor(rgb: 0x4492ff),
UIColor(rgb: 0x429bd5),
UIColor(rgb: 0x41a6a5),
UIColor(rgb: 0x3eb26d),
UIColor(rgb: 0x3dbd4a)
]
let accountContext = context.component.context
let present = context.component.present
let push = context.component.push
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 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: environment.theme.list.itemAccentColor,
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 textSideInset: CGFloat = 16.0
let forceDark = context.component.forceDark
let layoutPerks = {
size.height += 8.0
var i = 0
var perksItems: [AnyComponentWithIdentity<Empty>] = []
for perk in state.configuration.perks {
if case .business = context.component.mode, case .business = perk {
continue
}
let isNew = state.newPerks.contains(perk.identifier)
let titleComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: perk.title(strings: strings),
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))
let titleCombinedComponent: AnyComponent<Empty>
if isNew {
titleCombinedComponent = AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BadgeComponent(color: gradientColors[i], text: strings.Premium_New)))
], spacing: 5.0))
} else {
titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0))
}
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: titleCombinedComponent),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: perk.subtitle(strings: strings),
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: gradientColors[i],
foregroundColor: .white,
iconName: perk.iconName
))),
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
case .colors:
demoSubject = .colors
case .wallpapers:
demoSubject = .wallpapers
case .messageTags:
demoSubject = .messageTags
case .lastSeen:
demoSubject = .lastSeen
case .messagePrivacy:
demoSubject = .messagePrivacy
case .business:
demoSubject = .business
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
demoSubject = .doubleLimits
}
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: ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Premium_WhatsIncluded.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: perksItems
),
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)
.disappear(.default(alpha: true))
)
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
}
}
let layoutBusinessPerks = {
size.height += 8.0
let gradientColors: [UIColor] = [
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe54937),
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff),
UIColor(rgb: 0x676bff),
UIColor(rgb: 0x007aff)
]
var i = 0
var perksItems: [AnyComponentWithIdentity<Empty>] = []
for perk in state.configuration.businessPerks {
let isNew = state.newPerks.contains(perk.identifier)
let titleComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: perk.title(strings: strings),
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))
let titleCombinedComponent: AnyComponent<Empty>
if isNew {
titleCombinedComponent = AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BadgeComponent(color: gradientColors[i], text: strings.Premium_New)))
], spacing: 5.0))
} else {
titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0))
}
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: titleCombinedComponent),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: perk.subtitle(strings: strings),
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: gradientColors[min(i, gradientColors.count - 1)],
foregroundColor: .white,
iconName: perk.iconName
))),
action: { [weak state] _ in
let isPremium = state?.isPremium == true
if isPremium {
switch perk {
case .businessLocation:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .businessHours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .businessQuickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .businessGreetingMessage:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
})
case .businessAwayMessage:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
})
case .businessChatBots:
let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessIntro:
let _ = (accountContext.sharedContext.makeBusinessIntroSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessLinks:
let _ = (accountContext.sharedContext.makeBusinessLinksSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessLinksSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
fatalError()
}
} else {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .businessLocation:
demoSubject = .businessLocation
case .businessHours:
demoSubject = .businessHours
case .businessQuickReplies:
demoSubject = .businessQuickReplies
case .businessGreetingMessage:
demoSubject = .businessGreetingMessage
case .businessAwayMessage:
demoSubject = .businessAwayMessage
case .businessChatBots:
demoSubject = .businessChatBots
let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessIntro:
demoSubject = .businessIntro
let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessLinks:
demoSubject = .businessLinks
let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
fatalError()
}
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, 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)
}
}
))))
i += 1
}
let businessSection = businessSection.update(
component: ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: perksItems
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(businessSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + businessSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += businessSection.size.height
size.height += 23.0
}
let layoutMoreBusinessPerks = {
size.height += 8.0
let status = state.peer?.emojiStatus
let accentColor = environment.theme.list.itemAccentColor
var perksItems: [AnyComponentWithIdentity<Empty>] = []
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_SetEmojiStatus,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_SetEmojiStatusInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x676bff),
foregroundColor: .white,
iconName: "Premium/BusinessPerk/Status"
))),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: context.component.context,
color: accentColor,
fileId: status?.fileId,
file: nil
)))),
accessory: nil,
action: { [weak state] view in
guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return
}
state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor)
}
))))
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_TagYourChats,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_TagYourChatsInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x4492ff),
foregroundColor: .white,
iconName: "Premium/BusinessPerk/Tag"
))),
action: { _ in
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
}
))))
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_AddPost,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_AddPostInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x41a6a5),
foregroundColor: .white,
iconName: "Premium/Perk/Stories"
))),
action: { _ in
push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false))
}
))))
let moreBusinessSection = moreBusinessSection.update(
component: ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_MoreFeaturesTitle.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Business_MoreFeaturesInfo,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: perksItems
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(moreBusinessSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + moreBusinessSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += moreBusinessSection.size.height
size.height += 23.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)
})
let layoutAdsSettings = {
size.height += 8.0
var adsSettingsItems: [AnyComponentWithIdentity<Empty>] = []
adsSettingsItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Business_DontHideAds,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: false, action: { [weak state] value in
let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone()
state?.updated(transition: .immediate)
})),
action: nil
))))
let adsSettingsSection = adsSettingsSection.update(
component: ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_AdsTitle.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .markdown(text: environment.strings.Business_AdsInfo, attributes: termsMarkdownAttributes),
maximumNumberOfLines: 0
)),
items: adsSettingsItems
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(adsSettingsSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + adsSettingsSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += adsSettingsSection.size.height
size.height += 23.0
}
let copyLink = context.component.copyLink
if case .emojiStatus = context.component.source {
layoutPerks()
layoutOptions()
} else if case let .gift(fromPeerId, _, _, giftCode) = context.component.source {
if let giftCode, fromPeerId != context.component.context.account.peerId, !context.component.justBought {
let link = "https://t.me/giftcode/\(giftCode.slug)"
let linkButton = linkButton.update(
component: Button(
content: AnyComponent(
GiftLinkButtonContentComponent(theme: environment.theme, text: link, isSeparateSection: true)
),
action: {
copyLink(link)
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: .immediate
)
context.add(linkButton
.position(CGPoint(x: availableWidth / 2.0, y: size.height + linkButton.size.height / 2.0))
.disappear(.default(alpha: true))
)
size.height += linkButton.size.height
size.height += 17.0
}
layoutPerks()
} else {
layoutOptions()
if case .business = context.component.mode {
layoutBusinessPerks()
if context.component.isPremium == true {
layoutMoreBusinessPerks()
layoutAdsSettings()
}
} else {
layoutPerks()
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
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, completion: 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 case .business = context.component.mode, state.isPremium == false {
size.height += 123.0
}
if context.component.source != .settings {
size.height += 44.0
}
return size
}
}
}
private final class PremiumIntroScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: PremiumIntroScreen.Mode
let source: PremiumSource
let forceDark: Bool
let forceHasPremium: Bool
let updateInProgress: (Bool) -> Void
let present: (ViewController) -> Void
let push: (ViewController) -> Void
let completion: () -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
init(context: AccountContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) {
self.context = context
self.mode = mode
self.source = source
self.forceDark = forceDark
self.forceHasPremium = forceHasPremium
self.updateInProgress = updateInProgress
self.present = present
self.push = push
self.completion = completion
self.copyLink = copyLink
self.shareLink = shareLink
}
static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
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 source: PremiumSource
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.source = source
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 !self.inProgress else {
return
}
if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil {
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
self.paymentDisposable.set((self.context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug)
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let self else {
return
}
self.inProgress = false
self.updateInProgress(false)
self.updated(transition: .immediate)
if case let .waitForExpiration(date) = error {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Title, text: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Text(dateText).string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, action: { _ in return true }))
}
}, completed: { [weak self] in
guard let self else {
return
}
self.inProgress = false
self.justBought = true
self.updateInProgress(false)
self.updated(transition: .easeInOut(duration: 0.25))
self.completion()
}))
return
}
guard let inAppPurchaseManager = self.context.inAppPurchaseManager,
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }) 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 coin = Child(PremiumCoinComponent.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 .business = context.component.mode {
header = coin.update(
component: PremiumCoinComponent(
isIntro: isIntro,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
} else 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 .business = context.component.mode {
titleString = environment.strings.Business_Title
} else 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(peerId, _, file, maybeEmojiPack) = context.component.source, let emojiPack = maybeEmojiPack, case let .result(info, _, _) = emojiPack {
loadedEmojiPack = maybeEmojiPack
highlightableLinks = true
if peerId.isGroupOrChannel, otherPeerName.count > 20 {
otherPeerName = otherPeerName.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines) + "\u{2026}"
}
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] } ?? [], isEditing: false, expandIfNeeded: false, 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,
mode: context.component.mode,
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,
push: context.component.push,
selectProduct: { [weak state] productId in
state?.selectProduct(productId)
},
buy: { [weak state] in
state?.buy()
},
updateIsFocused: { [weak state] isFocused in
state?.updateIsFocused(isFocused)
},
copyLink: context.component.copyLink,
shareLink: context.component.shareLink
)),
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 isUnusedGift = false
if case let .gift(fromId, _, _, giftCode) = context.component.source {
if let giftCode, giftCode.usedDate == nil, fromId != context.component.context.account.peerId {
isUnusedGift = true
}
}
var buttonIsHidden = true
if !state.justBought {
if isUnusedGift {
buttonIsHidden = false
} else if state.canUpgrade {
buttonIsHidden = false
} else if !(state.isPremium ?? false) {
buttonIsHidden = false
}
}
if !buttonIsHidden {
let buttonTitle: String
if isUnusedGift {
buttonTitle = environment.strings.Premium_Gift_ApplyLink
} else 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 {
public enum Mode {
case premium
case business
}
fileprivate let context: AccountContext
fileprivate let mode: Mode
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, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) {
self.context = context
self.mode = mode
var updateInProgressImpl: ((Bool) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var presentImpl: ((ViewController) -> Void)?
var completionImpl: (() -> Void)?
var copyLinkImpl: ((String) -> Void)?
var shareLinkImpl: ((String) -> Void)?
super.init(context: context, component: PremiumIntroScreenComponent(
context: context,
mode: mode,
source: source,
forceDark: forceDark,
forceHasPremium: forceHasPremium,
updateInProgress: { inProgress in
updateInProgressImpl?(inProgress)
},
present: { c in
presentImpl?(c)
},
push: { c in
pushImpl?(c)
},
completion: {
completionImpl?()
},
copyLink: { link in
copyLinkImpl?(link)
},
shareLink: { link in
shareLinkImpl?(link)
}
), 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
if c is UndoOverlayController {
self?.present(c, in: .current)
} else {
self?.present(c, in: .window(.root))
}
}
pushImpl = { [weak self] c in
self?.push(c)
}
completionImpl = { [weak self] in
if let self {
self.animateSuccess()
}
}
copyLinkImpl = { [weak self] link in
UIPasteboard.general.string = link
guard let self else {
return
}
self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current)
}
shareLinkImpl = { [weak self] link in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true))
peerSelectionController.peerSelected = { [weak peerSelectionController, weak navigationController] peer, threadId in
if let _ = peerSelectionController {
Queue.mainQueue().after(0.88) {
HapticFeedback().success()
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
(navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root))
let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages)
|> deliverOnMainQueue).startStandalone()
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
}
}
}
navigationController.pushViewController(peerSelectionController)
}
if case .business = mode {
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
context.account.viewTracker.keepBusinessLinksApproximatelyUpdated()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
}
@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: PremiumCoinComponent.View.Tag()) as? PremiumCoinComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
} else 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
}
}
}
}
}
private final class BadgeComponent: CombinedComponent {
let color: UIColor
let text: String
init(
color: UIColor,
text: String
) {
self.color = color
self.text = text
}
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
static var body: Body {
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: component.text, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeSize = CGSize(width: badgeText.size.width + 7.0, height: 16.0)
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
color: component.color,
cornerRadius: 5.0
),
availableSize: badgeSize,
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0))
)
context.add(badgeText
.position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0))
)
return badgeSize
}
}
}