import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import PresentationDataUtils import ComponentFlow import ViewControllerComponent import MultilineTextComponent import BalancedTextComponent import BundleIconComponent import ButtonComponent import TextFormat import InAppPurchaseManager import ConfettiEffect import PremiumCoinComponent import Markdown import CountrySelectionUI import AccountContext final class AuthorizationSequencePaymentScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let sharedContext: SharedAccountContext let engine: TelegramEngineUnauthorized let inAppPurchaseManager: InAppPurchaseManager let presentationData: PresentationData let phoneNumber: String let phoneCodeHash: String let storeProduct: String init( sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, presentationData: PresentationData, phoneNumber: String, phoneCodeHash: String, storeProduct: String ) { self.sharedContext = sharedContext self.engine = engine self.inAppPurchaseManager = inAppPurchaseManager self.presentationData = presentationData self.phoneNumber = phoneNumber self.phoneCodeHash = phoneCodeHash self.storeProduct = storeProduct } static func ==(lhs: AuthorizationSequencePaymentScreenComponent, rhs: AuthorizationSequencePaymentScreenComponent) -> Bool { if lhs.storeProduct != rhs.storeProduct { return false } return true } final class View: UIView { private let animation = ComponentView() private let title = ComponentView() private let list = ComponentView() private let check = ComponentView() private let button = ComponentView() private var isUpdating: Bool = false private var component: AuthorizationSequencePaymentScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private var products: [InAppPurchaseManager.Product] = [] private var productsDisposable: Disposable? private var inProgress = false override init(frame: CGRect) { super.init(frame: frame) self.disablesInteractiveKeyboardGestureRecognizer = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.productsDisposable?.dispose() } private func proceed() { guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }) else { return } self.inProgress = true self.state?.updated() let (currency, amount) = storeProduct.priceCurrencyAndAmount let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount) let _ = (component.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in guard let self else { return } let presentationData = component.presentationData if available { let _ = (component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in let _ = status let _ = self }, error: { [weak self] error in guard let self, let controller = self.environment?.controller() else { return } self.inProgress = false self.state?.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 .tryLater: errorText = presentationData.strings.Premium_Purchase_ErrorUnknown case .cancelled: break } if let errorText { //addAppLogEvent(postbox: component.engine.account.postbox, type: "premium_gift.promo_screen_fail") let _ = errorText let _ = controller //let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) //controller.present(alertController, in: .window(.root)) } }) } else { self.inProgress = false self.state?.updated(transition: .immediate) } }) } func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment self.state = state if self.component == nil { self.productsDisposable = (component.inAppPurchaseManager.availableProducts |> deliverOnMainQueue).start(next: { [weak self] products in guard let self else { return } self.products = products if !self.isUpdating { self.state?.updated() } }) } self.component = component if themeUpdated { self.backgroundColor = environment.theme.list.plainBackgroundColor } let sideInset: CGFloat = 16.0 + environment.safeInsets.left let animationSize = self.animation.update( transition: transition, component: AnyComponent(PremiumCoinComponent( mode: .business, isIntro: true, isVisible: true, hasIdleAnimations: true )), environment: {}, containerSize: CGSize(width: min(414.0, availableSize.width), height: 184.0) ) let titleSize = self.title.update( transition: transition, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let textColor = environment.theme.list.itemPrimaryTextColor let secondaryTextColor = environment.theme.list.itemSecondaryTextColor let linkColor = environment.theme.list.itemAccentColor var countryName: String = "" if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(component.phoneNumber, preferredCountries: [:]) { countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: environment.strings) ?? country.name } var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( id: "cost", component: AnyComponent(ParagraphComponent( title: environment.strings.Login_Fee_SmsCost_Title, titleColor: textColor, text: environment.strings.Login_Fee_SmsCost_Text(countryName).string, textColor: secondaryTextColor, iconName: "Premium/Authorization/Cost", iconColor: linkColor )) ) ) items.append( AnyComponentWithIdentity( id: "verification", component: AnyComponent(ParagraphComponent( title: environment.strings.Login_Fee_Verification_Title, titleColor: textColor, text: environment.strings.Login_Fee_Verification_Text, textColor: secondaryTextColor, iconName: "Premium/Authorization/Verification", iconColor: linkColor )) ) ) items.append( AnyComponentWithIdentity( id: "support", component: AnyComponent(ParagraphComponent( title: environment.strings.Login_Fee_Support_Title, titleColor: textColor, text: environment.strings.Login_Fee_Support_Text, textColor: secondaryTextColor, iconName: "Premium/Authorization/Support", iconColor: linkColor, action: { [weak self] in guard let self, let controller = self.environment?.controller(), let product = self.products.first(where: { $0.id == component.storeProduct }) else { return } let introController = component.sharedContext.makePremiumIntroController( sharedContext: component.sharedContext, engine: component.engine, inAppPurchaseManager: component.inAppPurchaseManager, source: .auth(product.price), proceed: { [weak self] in self?.proceed() } ) controller.push(introController) } )) ) ) let listSize = self.list.update( transition: transition, component: AnyComponent(List(items)), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) let buttonHeight: CGFloat = 50.0 let bottomPanelPadding: CGFloat = 12.0 let titleSpacing: CGFloat = -24.0 let listSpacing: CGFloat = 12.0 let totalHeight = animationSize.height + titleSpacing + titleSize.height + listSpacing + listSize.height var originY = floor((availableSize.height - buttonHeight - bottomPanelPadding * 2.0 - totalHeight) / 2.0) if let animationView = self.animation.view { if animationView.superview == nil { self.addSubview(animationView) } animationView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) / 2.0), y: originY), size: animationSize) originY += animationSize.height + titleSpacing } if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: originY), size: titleSize) originY += titleSize.height + listSpacing } if let listView = self.list.view { if listView.superview == nil { self.addSubview(listView) } listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize) } let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset let priceString: String if let product = self.products.first(where: { $0.id == component.storeProduct }) { priceString = product.price } else { priceString = "–" } let buttonString = environment.strings.Login_Fee_SignUp(priceString).string let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(buttonString), component: AnyComponent( VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_GetPremiumForAWeek, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) ], spacing: 1.0) ) ), isEnabled: true, displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) ) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) } buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) } return availableSize } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class AuthorizationSequencePaymentScreen: ViewControllerComponentContainer { public init( sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, presentationData: PresentationData, inAppPurchaseManager: InAppPurchaseManager, phoneNumber: String, phoneCodeHash: String, storeProduct: String, back: @escaping () -> Void ) { super.init(component: AuthorizationSequencePaymentScreenComponent( sharedContext: sharedContext, engine: engine, inAppPurchaseManager: inAppPurchaseManager, presentationData: presentationData, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData))) loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: engine, completion: { [weak self] in if let strongSelf = self { strongSelf.requestLayout(forceUpdate: true, transition: .immediate) } }) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.attemptNavigation = { _ in return false } self.navigationBar?.backPressed = { back() } } public override func loadDisplayNode() { super.loadDisplayNode() self.displayNode.view.disableAutomaticKeyboardHandling = [.forward, .backward] } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } @objc private func cancelPressed() { self.dismiss() } } private final class ParagraphComponent: CombinedComponent { let title: String let titleColor: UIColor let text: String let textColor: UIColor let iconName: String let iconColor: UIColor let action: (() -> Void)? public init( title: String, titleColor: UIColor, text: String, textColor: UIColor, iconName: String, iconColor: UIColor, action: (() -> Void)? = nil ) { self.title = title self.titleColor = titleColor self.text = text self.textColor = textColor self.iconName = iconName self.iconColor = iconColor self.action = action } static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { if lhs.title != rhs.title { return false } if lhs.titleColor != rhs.titleColor { return false } if lhs.text != rhs.text { return false } if lhs.textColor != rhs.textColor { return false } if lhs.iconName != rhs.iconName { return false } if lhs.iconColor != rhs.iconColor { return false } return true } final class State: ComponentState { var cachedChevronImage: (UIImage, UIColor)? } func makeState() -> State { return State() } static var body: Body { let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) return { context in let component = context.component let state = context.state let leftInset: CGFloat = 64.0 let rightInset: CGFloat = 32.0 let textSideInset: CGFloat = leftInset + 8.0 let spacing: CGFloat = 5.0 let textTopInset: CGFloat = 9.0 let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let titleColor = component.titleColor let textColor = component.textColor let linkColor = component.iconColor let titleMarkdownAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: titleColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } ) if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== linkColor { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, linkColor) } let titleAttributedString = parseMarkdownIntoAttributedString(component.title, attributes: titleMarkdownAttributes).mutableCopy() as! NSMutableAttributedString if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string)) } let title = title.update( component: MultilineTextComponent( text: .plain(titleAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 1, highlightColor: linkColor.withAlphaComponent(0.1), highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) } else { return nil } }, tapAction: { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { component.action?() } } ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) let textMarkdownAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } ) let text = text.update( component: MultilineTextComponent( text: .markdown(text: component.text, attributes: textMarkdownAttributes), horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: linkColor.withAlphaComponent(0.1), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) } else { return nil } }, tapAction: { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { component.action?() } } ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), transition: .immediate ) let icon = icon.update( component: BundleIconComponent( name: component.iconName, tintColor: component.iconColor ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) ) context.add(icon .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) ) return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 18.0) } } }