diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9d3f7de0ca..ee4c3c06b1 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -441,7 +441,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU return } var dismissImpl: (() -> Void)? - let controller = AuthConfirmationScreen(context: self.context, subject: result, completion: { [weak self] accountContext, accountPeer, authResult in + let controller = AuthConfirmationScreen(context: self.context, requestSubject: subject, subject: result, completion: { [weak self] accountContext, accountPeer, authResult in guard let self else { return } @@ -503,6 +503,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU case .decline: let _ = self.context.engine.messages.declineUrlAuth(url: url).start() self.webView.sendEvent(name: "oauth_result_failed", data: nil) + case .failed: + guard case let .request(domain, _, _, _, _, _) = result else { + return + } + let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_Text(domain).string, cancel: nil, destructive: false), action: { _ in return true }) + if let navigationController = self.getNavigationController() { + (navigationController.topViewController as? ViewController)?.present(controller, in: .window(.root)) + } + + let _ = self.context.engine.messages.declineUrlAuth(url: url).start() + self.webView.sendEvent(name: "oauth_result_failed", data: nil) + + HapticFeedback().error() } }) dismissImpl = { [weak controller] in diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 98f02efe3a..7721f5bdb1 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -102,8 +102,8 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope if !skipSafari { options.append(OpenInOption(identifier: "safari", application: .safari, action: { var url = url - if url.hasPrefix("http://") || url.hasPrefix("https://") { - url = url.replacingOccurrences(of: "https://", with: "x-safari-https") + if url.hasPrefix("https://") { + url = url.replacingOccurrences(of: "https://", with: "x-safari-https://") } return .openUrl(url: url) })) diff --git a/submodules/TelegramApi/Sources/Api40.swift b/submodules/TelegramApi/Sources/Api40.swift index 7dcb134207..a696afe4c2 100644 --- a/submodules/TelegramApi/Sources/Api40.swift +++ b/submodules/TelegramApi/Sources/Api40.swift @@ -5322,6 +5322,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func checkUrlAuthMatchCode(url: String, matchCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-911967477) + serializeString(url, buffer: buffer, boxed: false) + serializeString(matchCode, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.checkUrlAuthMatchCode", parameters: [("url", String(describing: url)), ("matchCode", String(describing: matchCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func clearAllDrafts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index ca0c925309..2a8adc463b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -178,6 +178,7 @@ public enum MessageActionUrlAuthResult { public static let requestWriteAccess = Flags(rawValue: 1 << 0) public static let requestPhoneNumber = Flags(rawValue: 1 << 1) + public static let showMatchCodesFirst = Flags(rawValue: 1 << 2) } public struct ClientData : Equatable { @@ -244,25 +245,29 @@ func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageAct return .default } switch result { - case .urlAuthResultDefault: - return .default - case let .urlAuthResultAccepted(urlAuthResultAcceptedData): - let url = urlAuthResultAcceptedData.url - return .accepted(url: url) - case let .urlAuthResultRequest(urlAuthResultRequestData): - let (apiFlags, bot, domain) = (urlAuthResultRequestData.flags, urlAuthResultRequestData.bot, urlAuthResultRequestData.domain) - var clientData: MessageActionUrlAuthResult.ClientData? - if let browser = urlAuthResultRequestData.browser, let platform = urlAuthResultRequestData.platform, let ip = urlAuthResultRequestData.ip, let region = urlAuthResultRequestData.region { - clientData = MessageActionUrlAuthResult.ClientData(browser: browser, platform: platform, ip: ip, region: region) - } - var flags: MessageActionUrlAuthResult.Flags = [] - if (apiFlags & (1 << 0)) != 0 { - flags.insert(.requestWriteAccess) - } - if (apiFlags & (1 << 1)) != 0 { - flags.insert(.requestPhoneNumber) - } - return .request(domain: domain, bot: TelegramUser(user: bot), clientData: clientData, flags: flags, matchCodes: urlAuthResultRequestData.matchCodes, userIdHint: urlAuthResultRequestData.userIdHint.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }) + case .urlAuthResultDefault: + return .default + case let .urlAuthResultAccepted(urlAuthResultAcceptedData): + let url = urlAuthResultAcceptedData.url + return .accepted(url: url) + case let .urlAuthResultRequest(urlAuthResultRequestData): + let (apiFlags, bot, domain) = (urlAuthResultRequestData.flags, urlAuthResultRequestData.bot, urlAuthResultRequestData.domain) + var clientData: MessageActionUrlAuthResult.ClientData? + if let browser = urlAuthResultRequestData.browser, let platform = urlAuthResultRequestData.platform, let ip = urlAuthResultRequestData.ip, let region = urlAuthResultRequestData.region { + clientData = MessageActionUrlAuthResult.ClientData(browser: browser, platform: platform, ip: ip, region: region) + } + var flags: MessageActionUrlAuthResult.Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.requestWriteAccess) + } + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.requestPhoneNumber) + } + + if (apiFlags & (1 << 5)) != 0 { + flags.insert(.showMatchCodesFirst) + } + return .request(domain: domain, bot: TelegramUser(user: bot), clientData: clientData, flags: flags, matchCodes: urlAuthResultRequestData.matchCodes, userIdHint: urlAuthResultRequestData.userIdHint.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }) } } } @@ -327,3 +332,13 @@ func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActi } } } + +func _internal_checkUrlAuthMatchCode(account: Account, url: String, matchCode: String) -> Signal { + return account.network.request(Api.functions.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { result in + return result == .boolTrue + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index fd987c94ae..2bfbbd482b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -106,6 +106,10 @@ public extension TelegramEngine { public func declineUrlAuth(url: String) -> Signal { return _internal_declineUrlAuth(account: self.account, url: url) } + + public func checkUrlAuthMatchCode(url: String, matchCode: String) -> Signal { + return _internal_checkUrlAuthMatchCode(account: self.account, url: url, matchCode: matchCode) + } public func searchMessages(location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId? = nil, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit) diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD index 0dd57b9b18..4136ffb3e3 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD @@ -36,6 +36,8 @@ swift_library( "//submodules/AccountUtils", "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift index 8b97bdbdf8..6e10b9d8f9 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -25,22 +25,27 @@ import AccountUtils import GlassBackgroundComponent import AccountPeerContextItem import ActivityIndicator +import LottieComponent +import LottieComponentResourceContent private final class AuthConfirmationSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let requestSubject: MessageActionUrlSubject let subject: MessageActionUrlAuthResult let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void let cancel: (Bool) -> Void init( context: AccountContext, + requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void, cancel: @escaping (Bool) -> Void ) { self.context = context + self.requestSubject = requestSubject self.subject = subject self.completion = completion self.cancel = cancel @@ -55,7 +60,9 @@ private final class AuthConfirmationSheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let requestSubject: MessageActionUrlSubject private let subject: MessageActionUrlAuthResult + private let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void var peer: EnginePeer? var forcedAccount: (AccountContext, EnginePeer)? @@ -68,9 +75,11 @@ private final class AuthConfirmationSheetContent: CombinedComponent { var matchCodes: [String]? var selectedMatchCode: String? - init(context: AccountContext, subject: MessageActionUrlAuthResult) { + init(context: AccountContext, requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void) { self.context = context + self.requestSubject = requestSubject self.subject = subject + self.completion = completion super.init() @@ -82,6 +91,65 @@ private final class AuthConfirmationSheetContent: CombinedComponent { self.peer = peer self.updated() }) + + if case let .request(_, _, _, flags, matchCodes, _) = self.subject, let matchCodes, flags.contains(.showMatchCodesFirst) { + self.displayEmoji = true + self.matchCodes = matchCodes.shuffled() + } + + if case let .request(_, _, _, _, _, userIdHint) = self.subject, let userIdHint, userIdHint != context.account.peerId { + let _ = (activeAccountsAndPeers(context: self.context, includePrimary: true) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] primary, other in + guard let self else { + return + } + for (accountContext, peer, _) in other { + if peer.id == userIdHint { + self.forcedAccount = (accountContext, peer) + self.updated() + + accountContext.account.shouldBeServiceTaskMaster.set(.single(.now)) + let _ = accountContext.engine.messages.requestMessageActionUrlAuth(subject: requestSubject).start() + break + } + } + }) + } + } + + deinit { + if !self.inProgress { + if let (context, _) = self.forcedAccount { + context.account.shouldBeServiceTaskMaster.set(.single(.never)) + } + } + } + + func checkMatchCode(_ matchCode: String) { + guard case let .url(url, _) = self.requestSubject else { + return + } + self.selectedMatchCode = matchCode + self.updated(transition: .easeInOut(duration: 0.2)) + + let _ = (self.context.engine.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + if result { + self.displayEmoji = false + self.updated(transition: .spring(duration: 0.4)) + } else { + let accountContext = self.forcedAccount?.0 ?? self.context + guard let accountPeer = self.forcedAccount?.1 ?? self.peer else { + return + } + + self.completion(accountContext, accountPeer, .failed) + } + }) } func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) { @@ -132,6 +200,10 @@ private final class AuthConfirmationSheetContent: CombinedComponent { guard let self else { return } + if let (context, _) = self.forcedAccount { + context.account.shouldBeServiceTaskMaster.set(.single(.never)) + } + self.forcedAccount = nil self.updated() }), true)) @@ -147,6 +219,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent { guard let self else { return } + if let (context, _) = self.forcedAccount { + context.account.shouldBeServiceTaskMaster.set(.single(.never)) + } + + accountContext.account.shouldBeServiceTaskMaster.set(.single(.now)) + self.forcedAccount = (accountContext, peer) self.updated() }), true)) @@ -161,7 +239,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, subject: self.subject) + return State(context: self.context, requestSubject: self.requestSubject, subject: self.subject, completion: self.completion) } static var body: Body { @@ -191,13 +269,10 @@ private final class AuthConfirmationSheetContent: CombinedComponent { } let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } - let _ = strings - guard case let .request(domain, bot, clientData, flags, matchCodes, userIdHint) = component.subject else { + guard case let .request(domain, bot, clientData, flags, matchCodes, _) = component.subject else { fatalError() } - - let _ = userIdHint let sideInset: CGFloat = 16.0 + environment.safeInsets.left @@ -241,7 +316,6 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ) 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) ) } @@ -338,12 +412,28 @@ private final class AuthConfirmationSheetContent: CombinedComponent { )) ) } - items.append( - AnyComponentWithIdentity(id: "icon", component: AnyComponent( - Text(text: code, font: Font.regular(32.0), color: .black) - )) - ) + var file: TelegramMediaFile? + if let item = component.context.animatedEmojiStickersValue[code] { + file = item.first?.file._parse() + } else if let item = component.context.animatedEmojiStickersValue[code.strippedEmoji] { + file = item.first?.file._parse() + } + if let file { + items.append( + AnyComponentWithIdentity(id: "animatedIcon", component: AnyComponent( + LottieComponent(content: LottieComponent.ResourceContent(context: component.context, file: file, attemptSynchronously: true, providesPlaceholder: true), placeholderColor: theme.list.mediaPlaceholderColor, startingPosition: .begin, size: CGSize(width: 32.0, height: 32.0), loop: true, playOnce: nil) + )) + ) + } else { + items.append( + AnyComponentWithIdentity(id: "staticIcon", component: AnyComponent( + Text(text: code, font: Font.regular(32.0), color: .black) + )) + ) + } + + let subject = component.subject let emoji = emojis[code].update( component: AnyComponent( PlainButtonComponent( @@ -351,9 +441,17 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ZStack(items) ), minSize: emojiSize, - action: { - complete(code) + action: { [weak state] in + guard let state else { + return + } + if case let .request(_, _, _, flags, _, _) = subject, flags.contains(.showMatchCodesFirst) { + state.checkMatchCode(code) + } else { + complete(code) + } }, + isEnabled: state.selectedMatchCode == nil, animateAlpha: false, animateScale: true ) @@ -364,6 +462,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ) context.add(emoji .position(CGPoint(x: emojiOriginX + emojiSize.width / 2.0, y: contentHeight + emojiSize.height / 2.0)) + .opacity(state.selectedMatchCode != nil && state.selectedMatchCode != code ? 0.6 : 1.0) .appear(ComponentTransition.Appear({ _, view, transition in if !transition.animation.isImmediate { transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: emojiDelay) @@ -631,17 +730,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent { 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) + 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), @@ -669,12 +763,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent { guard let state else { return } - if let matchCodes, !matchCodes.isEmpty { + if !flags.contains(.showMatchCodesFirst), let matchCodes, !matchCodes.isEmpty { state.displayEmoji = true state.matchCodes = matchCodes.shuffled() state.updated(transition: .spring(duration: 0.4)) } else { - complete(nil) + complete(state.selectedMatchCode) } } ), @@ -699,15 +793,18 @@ private final class AuthConfirmationSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let requestSubject: MessageActionUrlSubject let subject: MessageActionUrlAuthResult let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void init( context: AccountContext, + requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void ) { self.context = context + self.requestSubject = requestSubject self.subject = subject self.completion = completion } @@ -731,6 +828,7 @@ private final class AuthConfirmationSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(AuthConfirmationSheetContent( context: context.component.context, + requestSubject: context.component.requestSubject, subject: context.component.subject, completion: context.component.completion, cancel: { animate in @@ -792,18 +890,22 @@ public class AuthConfirmationScreen: ViewControllerComponentContainer { public enum Result { case accept(allowWriteAccess: Bool, sharePhoneNumber: Bool, matchCode: String?) case decline + case failed } private let context: AccountContext + private let requestSubject: MessageActionUrlSubject private let subject: MessageActionUrlAuthResult fileprivate let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void public init( context: AccountContext, + requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void ) { self.context = context + self.requestSubject = requestSubject self.subject = subject self.completion = completion @@ -811,6 +913,7 @@ public class AuthConfirmationScreen: ViewControllerComponentContainer { context: context, component: AuthConfirmationSheetComponent( context: context, + requestSubject: requestSubject, subject: subject, completion: completion ), diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 412f7bbc79..9effcb12f9 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1822,19 +1822,12 @@ func openResolvedUrlImpl( |> deliverOnMainQueue).start(next: { result in if case .request = result { var dismissImpl: (() -> Void)? - let controller = AuthConfirmationScreen(context: context, subject: result, completion: { accountContext, accountPeer, authResult in + let controller = AuthConfirmationScreen(context: context, requestSubject: subject, subject: result, completion: { accountContext, accountPeer, authResult in switch authResult { case let .accept(allowWriteAccess, sharePhoneNumber, matchCode): - let signal: Signal - if accountContext === context { - signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode) - } else { - accountContext.account.shouldBeServiceTaskMaster.set(.single(.now)) - signal = accountContext.engine.messages.requestMessageActionUrlAuth(subject: subject) - |> castError(MessageActionUrlAuthError.self) - |> mapToSignal { result in - return accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode) - } |> afterDisposed { + let signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode) + |> afterDisposed { + if accountContext !== context { accountContext.account.shouldBeServiceTaskMaster.set(.single(.never)) } } @@ -1882,8 +1875,9 @@ func openResolvedUrlImpl( } } + let isWebUrl = url.hasPrefix("http:") || url.hasPrefix("https:") let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) - if let match = openInOptions.first(where: { $0.identifier == browserIdentifier }), case let .openUrl(openUrl) = match.action() { + if isWebUrl, let match = openInOptions.first(where: { $0.identifier == browserIdentifier }), case let .openUrl(openUrl) = match.action() { context.sharedContext.openExternalUrl(context: context, urlContext: .external, url: openUrl, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } else { context.sharedContext.openExternalUrl(context: context, urlContext: .external, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) @@ -1901,6 +1895,15 @@ func openResolvedUrlImpl( }) case .decline: let _ = context.engine.messages.declineUrlAuth(url: url).start() + case .failed: + dismissImpl?() + + if case let .request(domain, _, _, _, _, _) = result { + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_Text(domain).string, timeout: nil, customUndoText: nil), action: { _ in return true }) + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) + } + + HapticFeedback().error() } }) navigationController?.pushViewController(controller)