import Foundation import UIKit import Display import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import ComponentFlow import ViewControllerComponent import SheetComponent import MultilineTextComponent import GlassBarButtonComponent import ButtonComponent import PresentationDataUtils import BundleIconComponent import ListSectionComponent import ListActionItemComponent import AvatarComponent import Markdown import PhoneNumberFormat private final class AuthConfirmationSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: MessageActionUrlAuthResult let completion: (Bool, Bool) -> Void let cancel: (Bool) -> Void init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (Bool, Bool) -> Void, cancel: @escaping (Bool) -> Void ) { self.context = context self.subject = subject self.completion = completion self.cancel = cancel } static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool { if lhs.context !== rhs.context { return false } return true } final class State: ComponentState { private let context: AccountContext private let subject: MessageActionUrlAuthResult var peer: EnginePeer? fileprivate var inProgress = false var allowWrite = true weak var controller: ViewController? init(context: AccountContext, subject: MessageActionUrlAuthResult) { self.context = context self.subject = subject super.init() let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, let peer else { return } self.peer = peer self.updated() }) } func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) { guard case let .request(domain, _, _, _) = self.subject else { return } let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, case let .user(user) = peer, let phone = user.phone else { return } let phoneNumber = formatPhoneNumber(context: self.context, number: phone) //TODO:localize let alertController = textAlertController( context: self.context, title: "Phone Number", text: "**\(domain)** wants to access your phone number **\(phoneNumber)**.\n\nAllow access?", actions: [ TextAlertAction(type: .genericAction, title: "Deny", action: { commit(false) }), TextAlertAction(type: .defaultAction, title: "Allow", action: { commit(true) }) ] ) self.controller?.present(alertController, in: .window(.root)) }) } } func makeState() -> State { return State(context: self.context, subject: self.subject) } static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) let peerButton = Child(AvatarComponent.self) let avatar = Child(AvatarComponent.self) let title = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self) let clientSection = Child(ListSectionComponent.self) let optionsSection = Child(ListSectionComponent.self) let cancelButton = Child(ButtonComponent.self) let doneButton = Child(ButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let theme = environment.theme let strings = environment.strings let state = context.state if state.controller == nil { state.controller = environment.controller() } let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } let _ = strings guard case let .request(domain, bot, clientData, flags) = component.subject else { fatalError() } let sideInset: CGFloat = 16.0 + environment.safeInsets.left let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in component.cancel(true) } ), availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) if let peer = state.peer { let peerButton = peerButton.update( component: AvatarComponent( context: component.context, theme: environment.theme, peer: peer ), availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(peerButton .position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0)) ) } var contentHeight: CGFloat = 32.0 let avatar = avatar.update( component: AvatarComponent( context: component.context, theme: environment.theme, peer: EnginePeer(bot), clipStyle: .roundedRect ), availableSize: CGSize(width: 92.0, height: 92.0), transition: .immediate ) context.add(avatar .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0)) ) contentHeight += avatar.size.height contentHeight += 18.0 let titleFont = Font.bold(24.0) let title = title.update( component: MultilineTextComponent( text: .markdown(text: "Log in to **\(domain)**", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), horizontalAlignment: .center, maximumNumberOfLines: 2 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0)) ) contentHeight += title.size.height contentHeight += 16.0 let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let description = description.update( component: MultilineTextComponent( text: .markdown(text: "This site will receive your **name**,\n**username** and **profile photo**.", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), horizontalAlignment: .center, maximumNumberOfLines: 3, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(description .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0)) ) contentHeight += description.size.height contentHeight += 16.0 var clientSectionItems: [AnyComponentWithIdentity] = [] clientSectionItems.append( AnyComponentWithIdentity(id: "device", component: AnyComponent( ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "Device", font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 )), contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0), accessory: .custom(ListActionItemComponent.CustomAccessory( component: AnyComponentWithIdentity( id: "info", component: AnyComponent( VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: clientData?.platform ?? "", font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: clientData?.browser ?? "", font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0), textColor: theme.list.itemSecondaryTextColor )), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 ))) ], alignment: .right, spacing: 3.0) ) ), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), isInteractive: true )), action: nil ) )) ) clientSectionItems.append( AnyComponentWithIdentity(id: "region", component: AnyComponent( ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "IP Address", font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 )), contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0), accessory: .custom(ListActionItemComponent.CustomAccessory( component: AnyComponentWithIdentity( id: "info", component: AnyComponent( VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: clientData?.ip ?? "", font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: clientData?.region ?? "", font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0), textColor: theme.list.itemSecondaryTextColor )), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 ))) ], alignment: .right, spacing: 3.0) ) ), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), isInteractive: true )), action: nil ) )) ) let clientSection = clientSection.update( component: ListSectionComponent( theme: theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "This login attempt came from the device above.", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: clientSectionItems ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) context.add(clientSection .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0)) ) contentHeight += clientSection.size.height if flags.contains(.requestWriteAccess) { contentHeight += 38.0 var optionsSectionItems: [AnyComponentWithIdentity] = [] optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "Allow Messages", font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in guard let state else { return } state.allowWrite = !state.allowWrite state.updated() })), action: nil )))) let optionsSection = optionsSection.update( component: ListSectionComponent( theme: theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "This will allow \(EnginePeer(bot).compactDisplayTitle) to message you.", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: optionsSectionItems ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) context.add(optionsSection .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0)) ) contentHeight += optionsSection.size.height } contentHeight += 32.0 let buttonSpacing: CGFloat = 10.0 let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonWidth = (context.availableSize.width - buttonInsets.left - buttonInsets.right - buttonSpacing) / 2.0 let cancelButton = cancelButton.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), foreground: theme.list.itemPrimaryTextColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Cancel", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)))) ), action: { component.cancel(true) } ), availableSize: CGSize(width: buttonWidth, height: 52.0), transition: .immediate ) context.add(cancelButton .position(CGPoint(x: context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0)) ) let doneButton = doneButton.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), cornerRadius: 10.0, ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Log In", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), displaysProgress: state.inProgress, action: { [weak state] in guard let state else { return } var allowWrite = false if flags.contains(.requestWriteAccess) && state.allowWrite { allowWrite = true } if flags.contains(.requestPhoneNumber) { state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in component.completion(allowWrite, sharePhoneNumber) state.inProgress = true state.updated() }) } else { component.completion(allowWrite, false) state.inProgress = true state.updated() } } ), availableSize: CGSize(width: buttonWidth, height: 52.0), transition: .immediate ) context.add(doneButton .position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0)) ) contentHeight += doneButton.size.height contentHeight += buttonInsets.bottom return CGSize(width: context.availableSize.width, height: contentHeight) } } } private final class AuthConfirmationSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: MessageActionUrlAuthResult let completion: (Bool, Bool) -> Void init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (Bool, Bool) -> Void ) { self.context = context self.subject = subject self.completion = completion } static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } return true } static var body: Body { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(AuthConfirmationSheetContent( context: context.component.context, subject: context.component.subject, completion: context.component.completion, cancel: { animate in if animate { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else if let controller = controller() { controller.dismiss(animated: false, completion: nil) } } )), style: .glass, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public class AuthConfirmationScreen: ViewControllerComponentContainer { private let context: AccountContext private let subject: MessageActionUrlAuthResult fileprivate let completion: (Bool, Bool) -> Void public init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (Bool, Bool) -> Void ) { self.context = context self.subject = subject self.completion = completion super.init( context: context, component: AuthConfirmationSheetComponent( context: context, subject: subject, completion: completion ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.navigationPresentation = .flatModal } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func viewDidLoad() { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } }