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 PlainButtonComponent import PresentationDataUtils import BundleIconComponent import ListSectionComponent import ListActionItemComponent import AvatarComponent import Markdown import PhoneNumberFormat import ContextUI import AccountUtils import GlassBackgroundComponent import AccountPeerContextItem private final class AuthConfirmationSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: MessageActionUrlAuthResult let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void let cancel: (Bool) -> Void init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> 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? var forcedAccount: (AccountContext, EnginePeer)? fileprivate var inProgress = false var allowWrite = true weak var controller: ViewController? var displayEmoji = false var matchCodes: [String]? 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 presentationData = self.context.sharedContext.currentPresentationData.with { $0 } 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).replacingOccurrences(of: " ", with: "\u{00A0}") let alertController = textAlertController( context: self.context, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Title, text: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Text(domain, phoneNumber).string, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Deny, action: { commit(false) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Allow, action: { commit(true) }) ] ) self.controller?.present(alertController, in: .window(.root)) }) } func presentAccountSwitchMenu(sourceView: GlassContextExtractableContainer) { guard let controller = self.controller else { return } let context = self.context let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let items: Signal<[ContextMenuItem], NoError> = activeAccountsAndPeers(context: self.context, includePrimary: true) |> take(1) |> map { primary, other -> [ContextMenuItem] in var items: [ContextMenuItem] = [] var existingIds = Set() if let (_, peer) = primary { items.append(.custom(AccountPeerContextItem(context: context, account: context.account, peer: peer, action: { [weak self] _, f in f(.default) guard let self else { return } self.forcedAccount = nil self.updated() }), true)) existingIds.insert(peer.id) } for (accountContext, peer, _) in other { guard !existingIds.contains(peer.id) else { continue } items.append(.custom(AccountPeerContextItem(context: accountContext, account: accountContext.account, peer: peer, action: { [weak self] _, f in f(.default) guard let self else { return } self.forcedAccount = (accountContext, peer) self.updated() }), true)) } return items } let contextController = makeContextController(presentationData: presentationData, source: .reference(AuthConfirmationReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil) controller.presentInGlobalOverlay(contextController) } } func makeState() -> State { return State(context: self.context, subject: self.subject) } static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) let accountButton = Child(AccountSwitchComponent.self) let avatar = Child(AvatarComponent.self) let title = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self) let emojiTitle = Child(MultilineTextComponent.self) let emojiDescription = Child(MultilineTextComponent.self) let emojis = ChildMap(environment: Empty.self, keyedBy: AnyHashable.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, matchCodes, userIdHint) = component.subject else { fatalError() } let _ = userIdHint 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 accountButton = accountButton.update( component: AccountSwitchComponent( context: state.forcedAccount?.0 ?? component.context, theme: environment.theme, peer: state.forcedAccount?.1 ?? peer, canSwitch: true, isVisible: true, action: { [weak state] sourceView in state?.presentAccountSwitchMenu(sourceView: sourceView) } ), availableSize: context.availableSize, transition: .immediate ) context.add(accountButton .position(CGPoint(x: context.availableSize.width - 16.0 - accountButton.size.width / 2.0, y: 16.0 + accountButton.size.height / 2.0)) .scale(state.displayEmoji ? 0.0 : 1.0) ) } let titleFont = Font.bold(24.0) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let complete: (String?) -> Void = { [weak state] matchCode in guard let state else { return } var allowWrite = false if flags.contains(.requestWriteAccess) && state.allowWrite { allowWrite = true } let accountContext = state.forcedAccount?.0 ?? component.context guard let accountPeer = state.forcedAccount?.1 ?? state.peer else { return } if flags.contains(.requestPhoneNumber) { state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in component.completion(accountContext, accountPeer, .accept(allowWriteAccess: allowWrite, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode)) state.inProgress = true state.updated() }) } else { component.completion(accountContext, accountPeer, .accept(allowWriteAccess: allowWrite, sharePhoneNumber: false, matchCode: matchCode)) state.inProgress = true state.updated() } } var contentHeight: CGFloat = 32.0 if state.displayEmoji, let matchCodes = state.matchCodes { //TODO:localize contentHeight += 36.0 let emojiTitle = emojiTitle.update( component: MultilineTextComponent( text: .markdown(text: "Tap the emoji shown\non your other device", 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, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(emojiTitle .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + emojiTitle.size.height / 2.0)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) contentHeight += emojiTitle.size.height contentHeight += 16.0 let emojiDescription = emojiDescription.update( component: MultilineTextComponent( text: .markdown(text: "Telegram wants to make sure it's really you.", 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(emojiDescription .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + emojiDescription.size.height / 2.0)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) contentHeight += emojiDescription.size.height contentHeight += 48.0 var emojiDelay: Double = 0.0 let emojiSize = CGSize(width: 64.0, height: 64.0) let emojiSpacing: CGFloat = 36.0 let totalWidth = CGFloat(matchCodes.count) * emojiSize.width + CGFloat(matchCodes.count - 1) * emojiSpacing var emojiOriginX = context.availableSize.width / 2.0 - totalWidth / 2.0 for code in matchCodes { let emoji = emojis[code].update( component: AnyComponent( PlainButtonComponent( content: AnyComponent( ZStack([ AnyComponentWithIdentity(id: "background", component: AnyComponent( FilledRoundedRectangleComponent(color: theme.list.itemBlocksBackgroundColor, cornerRadius: .minEdge, smoothCorners: false) )), AnyComponentWithIdentity(id: "icon", component: AnyComponent( Text(text: code, font: Font.regular(32.0), color: .black) )) ]) ), minSize: emojiSize, action: { complete(code) }, animateAlpha: false, animateScale: true ) ), environment: {}, availableSize: emojiSize, transition: context.transition ) context.add(emoji .position(CGPoint(x: emojiOriginX + emojiSize.width / 2.0, y: contentHeight + emojiSize.height / 2.0)) .appear(ComponentTransition.Appear({ _, view, transition in if !transition.animation.isImmediate { transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: emojiDelay) transition.animateScale(view: view, from: 0.01, to: 1.0, delay: emojiDelay) } })) .disappear(.default(scale: true, alpha: true)) ) emojiOriginX += emojiSize.width + emojiSpacing emojiDelay += 0.08 } contentHeight += emojiSize.height contentHeight += 48.0 } else { 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)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) contentHeight += avatar.size.height contentHeight += 18.0 let title = title.update( component: MultilineTextComponent( text: .markdown(text: strings.AuthConfirmation_Title(domain).string, 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)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) contentHeight += title.size.height contentHeight += 16.0 let description = description.update( component: MultilineTextComponent( text: .markdown(text: strings.AuthConfirmation_Description, 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)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) 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: strings.AuthConfirmation_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: strings.AuthConfirmation_IpAddress, 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: strings.AuthConfirmation_Info, 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)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) 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: strings.AuthConfirmation_AllowMessages, 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: strings.AuthConfirmation_AllowMessagesInfo(EnginePeer(bot).compactDisplayTitle).string, 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)) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) 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) var buttonWidth = context.availableSize.width - buttonInsets.left - buttonInsets.right if !state.displayEmoji { buttonWidth = (buttonWidth - 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: strings.AuthConfirmation_Cancel, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)))) ), action: { [weak state] in guard let state else { return } if state.displayEmoji { state.displayEmoji = false state.updated(transition: .spring(duration: 0.4)) } else { let accountContext = state.forcedAccount?.0 ?? component.context guard let accountPeer = state.forcedAccount?.1 ?? state.peer else { return } component.completion(accountContext, accountPeer, .decline) component.cancel(true) } } ), availableSize: CGSize(width: buttonWidth, height: 52.0), transition: context.transition ) context.add(cancelButton .position(CGPoint(x: state.displayEmoji ? context.availableSize.width / 2.0 : 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: strings.AuthConfirmation_LogIn, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), displaysProgress: state.inProgress, action: { [weak state] in guard let state else { return } if let matchCodes, !matchCodes.isEmpty { state.displayEmoji = true state.matchCodes = matchCodes.shuffled() state.updated(transition: .spring(duration: 0.4)) } else { complete(nil) } } ), availableSize: CGSize(width: buttonWidth, height: 52.0), transition: context.transition ) 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)) .opacity(state.displayEmoji ? 0.0 : 1.0) .scale(state.displayEmoji ? 0.01 : 1.0) ) contentHeight += cancelButton.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: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> 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( metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, 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 { public enum Result { case accept(allowWriteAccess: Bool, sharePhoneNumber: Bool, matchCode: String?) case decline } private let context: AccountContext private let subject: MessageActionUrlAuthResult fileprivate let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void public init( context: AccountContext, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> 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() } } } private final class AuthConfirmationReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceView: UIView let forceDisplayBelowKeyboard = true init(controller: ViewController, sourceView: UIView) { self.controller = controller self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } }