diff --git a/Swiftgram/SGQrLogin/BUILD b/Swiftgram/SGQrLogin/BUILD new file mode 100644 index 00000000000..197509202af --- /dev/null +++ b/Swiftgram/SGQrLogin/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGQrLogin", + module_name = "SGQrLogin", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/QrCodeUI:QrCodeUI", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/ComponentFlow", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGQrLogin/Sources/SGQrLogin.swift b/Swiftgram/SGQrLogin/Sources/SGQrLogin.swift new file mode 100644 index 00000000000..6bddc7d6691 --- /dev/null +++ b/Swiftgram/SGQrLogin/Sources/SGQrLogin.swift @@ -0,0 +1,117 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import GlassBarButtonComponent +import ComponentFlow +import BundleIconComponent + +public func sgExportQrLoginToken(account: UnauthorizedAccount, sharedContext: SharedAccountContext) -> Signal { + return sharedContext.activeAccountContexts + |> castError(ExportAuthTransferTokenError.self) + |> take(1) + |> mapToSignal { activeAccountsAndInfo -> Signal in + let (_, activeAccounts, _) = activeAccountsAndInfo + let activeProductionUserIds = activeAccounts.map({ $0.1.account }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) + let activeTestingUserIds = activeAccounts.map({ $0.1.account }).filter({ $0.testingEnvironment }).map({ $0.peerId.id }) + + return TelegramEngineUnauthorized(account: account).auth.exportAuthTransferToken(accountManager: sharedContext.accountManager, otherAccountUserIds: account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds, syncContacts: true) + } +} + +// MARK: Swiftgram +// .authKeyUnregistered is a transient auth-key race after QR login's DC migration. +// Redoing the export/import round trip and resubmitting the password resolves it, +// usually in under two seconds. +public func sgAuthorizeWithPasswordRetryingQrLogin( + sharedContext: SharedAccountContext, + account: UnauthorizedAccount, + password: String, + syncContacts: Bool, + maxRetries: Int = 8, + retryDelay: Double = 1.5, + retryDelayIncrement: Double = 1.0, + maxRetryDelay: Double = 6.0, + accountUpdated: @escaping (UnauthorizedAccount) -> Void +) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + func attemptPassword(account: UnauthorizedAccount, retriesLeft: Int) { + disposable.set(authorizeWithPassword(accountManager: sharedContext.accountManager, account: account, password: password, syncContacts: syncContacts).startStrict(error: { error in + guard case .authKeyUnregistered = error, retriesLeft > 0 else { + subscriber.putError(error) + return + } + retryQrExportImport(account: account, retriesLeft: retriesLeft) + }, completed: { + subscriber.putCompletion() + })) + } + + func retryQrExportImport(account: UnauthorizedAccount, retriesLeft: Int) { + guard retriesLeft > 0 else { + subscriber.putError(.authKeyUnregistered) + return + } + disposable.set(sgExportQrLoginToken(account: account, sharedContext: sharedContext).startStrict(next: { result in + switch result { + case let .passwordRequested(newAccount): + accountUpdated(newAccount) + attemptPassword(account: newAccount, retriesLeft: retriesLeft - 1) + case let .changeAccountAndRetry(newAccount): + accountUpdated(newAccount) + retryQrExportImport(account: newAccount, retriesLeft: retriesLeft - 1) + case .loggedIn: + subscriber.putCompletion() + case .displayToken: + subscriber.putError(.authKeyUnregistered) + } + }, error: { error in + switch error { + case .authKeyUnregistered, .authTokenExpired: + let attemptIndex = maxRetries - retriesLeft + let currentDelay = min(maxRetryDelay, retryDelay + Double(attemptIndex) * retryDelayIncrement) + disposable.set((Signal.complete() + |> delay(currentDelay, queue: .mainQueue())).startStrict(completed: { + retryQrExportImport(account: account, retriesLeft: retriesLeft - 1) + })) + case .limitExceeded: + subscriber.putError(.limitExceeded) + case .generic: + subscriber.putError(.generic) + } + })) + } + + attemptPassword(account: account, retriesLeft: maxRetries) + + return disposable + } +} + +public func sgQrLoginBarButtonNode(theme: PresentationTheme, action: @escaping (UIView) -> Void) -> BarComponentHostNode { + let size = CGSize(width: 40.0, height: 40.0) + return BarComponentHostNode( + component: AnyComponentWithIdentity(id: "qrLogin", component: AnyComponent( + GlassBarButtonComponent( + size: size, + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "qrLoginIcon", component: AnyComponent( + BundleIconComponent( + name: "Settings/QrIcon", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: action + ) + )), + size: size + ) +} diff --git a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings index 0383460b65f..6f63874bb79 100644 --- a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings +++ b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings @@ -114,6 +114,7 @@ "Auth.AccountBackupReminder" = "Make sure you have a backup access method. Keep a SIM for SMS or an additional session logged in to avoid being locked out."; "Auth.UnofficialAppCodeTitle" = "You can get the code only with official app"; +"Auth.LoginRetryNotice" = "Couldn't complete sign-in. Please try logging in again."; "Settings.SmallReactions" = "Small Reactions"; "Settings.HideReactions" = "Hide Reactions"; diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index 47aec3ce644..82ff5af623f 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -1,7 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") sgdeps = [ - "//Swiftgram/SGStrings:SGStrings" + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGQrLogin:SGQrLogin", ] swift_library( @@ -35,7 +36,7 @@ swift_library( "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/ImageCompression:ImageCompression", "//submodules/RMIntro:RMIntro", - "//submodules/QrCode:QrCode", + "//submodules/QrCodeUI:QrCodeUI", "//submodules/PhoneInputNode:PhoneInputNode", "//submodules/CodeInputView:CodeInputView", "//submodules/DebugSettingsUI:DebugSettingsUI", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 75c29cf1c69..ff143b09d89 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -25,6 +25,7 @@ import AlertUI import InAppPurchaseManager import ObjectiveC import AVFoundation +import SGQrLogin private var ObjCKey_Delegate: Int? @@ -943,12 +944,15 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.loginWithPassword = { [weak self, weak controller] password in if let strongSelf = self { controller?.inProgress = true - - strongSelf.actionDisposable.set((authorizeWithPassword(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, password: password, syncContacts: syncContacts) |> deliverOnMainQueue).startStrict(error: { error in + + // MARK: Swiftgram + strongSelf.actionDisposable.set((sgAuthorizeWithPasswordRetryingQrLogin(sharedContext: strongSelf.sharedContext, account: strongSelf.account, password: password, syncContacts: syncContacts, accountUpdated: { [weak strongSelf] updatedAccount in + strongSelf?.account = updatedAccount + }) |> deliverOnMainQueue).startStrict(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { controller.inProgress = false - + let text: String switch error { case .limitExceeded: @@ -957,9 +961,26 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth text = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError case .generic: text = strongSelf.presentationData.strings.Login_UnknownError + // MARK: Swiftgram + case .authKeyUnregistered: + text = i18n("Auth.LoginRetryNotice", strongSelf.presentationData.strings.baseLanguageCode) } - - controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + // MARK: Swiftgram + let okAction: () -> Void + if case .authKeyUnregistered = error { + okAction = { + guard let strongSelf = self else { + return + } + let countryCode = AuthorizationSequenceCountrySelectionController.defaultCountryCode() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + } + } else { + okAction = {} + } + + controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: okAction)]), in: .window(.root)) controller.passwordIsInvalid() } } @@ -1326,6 +1347,10 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } + // MARK: Swiftgram + // Push underneath so there's a previousItem for the back arrow, + // otherwise password entry is a dead end when QR login lands here directly. + controllers.append(self.phoneEntryController(countryCode: AuthorizationSequenceCountrySelectionController.defaultCountryCode(), number: "", splashController: nil)) controllers.append(self.passwordEntryController(hint: hint, suggestReset: suggestReset, syncContacts: syncContacts)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .passwordRecovery(_, _, _, emailPattern, syncContacts): diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift index 913678b1a1a..8f4c5aff186 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift @@ -61,6 +61,14 @@ final class AuthorizationSequencePasswordEntryController: ViewController { self.navigationBar?.backPressed = { back() } + + // MARK: Swiftgram + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.sgBackPressed)) + } + + // MARK: Swiftgram + @objc private func sgBackPressed() { + self.navigationBar?.backPressed() } required init(coder aDecoder: NSCoder) { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index a6e158e9657..72e62db6989 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -13,6 +13,9 @@ import PhoneNumberFormat import DebugSettingsUI import MessageUI import AuthenticationServices +import QrCodeUI +import GlassBarButtonComponent +import SGQrLogin public final class AuthorizationSequencePhoneEntryController: ViewController, MFMailComposeViewControllerDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { private var controllerNode: AuthorizationSequencePhoneEntryControllerNode { @@ -34,6 +37,8 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF private let back: () -> Void private var currentData: (Int32, String?, String)? + // MARK: Swiftgram + private var qrButtonNode: BarComponentHostNode? var codeNode: ASDisplayNode { return self.controllerNode.codeNode @@ -93,7 +98,18 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF if !otherAccountPhoneNumbers.1.isEmpty { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } - + + // MARK: Swiftgram + // Escape hatch for when phone-code delivery is unusable. + // Safe to set once: updateNavigationItems() only touches rightBarButtonItem below 360pt width. + if account != nil { + let qrButtonNode = sgQrLoginBarButtonNode(theme: presentationData.theme, action: { [weak self] _ in + self?.qrLoginPressed() + }) + self.qrButtonNode = qrButtonNode + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: qrButtonNode) + } + if let countriesConfiguration { AuthorizationSequenceCountrySelectionController.setupCountryCodes(countries: countriesConfiguration.countries, codesByPrefix: countriesConfiguration.countriesByPrefix) } @@ -110,6 +126,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF @objc private func cancelPressed() { self.back() } + + // MARK: Swiftgram + @objc private func qrLoginPressed() { + self.view.endEditing(true) + self.controllerNode.beginQrLogin() + } func updateNavigationItems() { guard let layout = self.validLayout, layout.size.width < 360.0 else { @@ -163,7 +185,31 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF } self.loadAndPresentPasskey(force: true) } - + // MARK: Swiftgram + self.controllerNode.presentQrCode = { [weak self] urlString -> QrCodeScreen? in + guard let self else { + return nil + } + let screen = QrCodeScreen(sharedContext: self.sharedContext, updatedPresentationData: (self.presentationData, .single(self.presentationData)), subject: .loginToken(url: urlString)) + screen.didDismiss = { [weak self] in + self?.controllerNode.qrLoginDismissed() + } + self.push(screen) + return screen + } + self.controllerNode.qrLoginError = { [weak self] error in + guard let self else { + return + } + let text: String + switch error { + case .limitExceeded: + text = self.presentationData.strings.Login_CodeFloodError + case .authKeyUnregistered, .authTokenExpired, .generic: + text = self.presentationData.strings.Login_UnknownError + } + self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } if let (code, name, number) = self.currentData { self.controllerNode.codeAndNumber = (code, name, number) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 866efd7acea..bfb14babe5f 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -6,7 +6,6 @@ import TelegramCore import TelegramPresentationData import PhoneInputNode import CountrySelectionUI -import QrCode import SwiftSignalKit import AccountContext import AnimatedStickerNode @@ -15,6 +14,8 @@ import SolidRoundedButtonNode import AuthorizationUtils import ManagedAnimationNode import Markdown +import SGQrLogin +import QrCodeUI private final class PhoneAndCountryNode: ASDisplayNode { let strings: PresentationStrings @@ -320,11 +321,17 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { private let contactSyncNode: ContactSyncNode private let proceedNode: SolidRoundedButtonNode - private var qrNode: ASImageNode? private let exportTokenDisposable = MetaDisposable() private let tokenEventsDisposable = MetaDisposable() var accountUpdated: ((UnauthorizedAccount) -> Void)? - + // MARK: Swiftgram + var presentQrCode: ((String) -> QrCodeScreen?)? + var qrLoginError: ((ExportAuthTransferTokenError) -> Void)? + private weak var presentedQrCodeScreen: QrCodeScreen? + private let qrRefreshTimerDisposable = MetaDisposable() + private let qrErrorRetryDisposable = MetaDisposable() + private var qrErrorRetryAttempt: Int = 0 + var retryPasskey: (() -> Void)? private let debugAction: () -> Void @@ -480,13 +487,6 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } } - if let account = account { - self.tokenEventsDisposable.set((account.updateLoginTokenEvents - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - self?.refreshQrToken() - })) - } - self.proceedNode.pressed = { [weak self] in self?.checkPhone?() } @@ -500,15 +500,13 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { deinit { self.exportTokenDisposable.dispose() self.tokenEventsDisposable.dispose() + self.qrRefreshTimerDisposable.dispose() } override func didLoad() { super.didLoad() self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:)))) - #if DEBUG && false - self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:)))) - #endif } private var animationSnapshotView: UIView? @@ -700,65 +698,63 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } } - @objc private func debugQrTap(_ recognizer: UITapGestureRecognizer) { - if self.qrNode == nil { - let qrNode = ASImageNode() - qrNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 64.0 + 16.0), size: CGSize(width: 200.0, height: 200.0)) - self.qrNode = qrNode - self.addSubnode(qrNode) - - self.refreshQrToken() - } + // MARK: Swiftgram + func beginQrLogin() { + self.refreshQrToken() } - + + // MARK: Swiftgram + func qrLoginDismissed() { + self.stopQrLoop() + self.presentedQrCodeScreen = nil + } + + // MARK: Swiftgram + private func stopQrLoop() { + self.exportTokenDisposable.set(nil) + self.qrRefreshTimerDisposable.set(nil) + self.tokenEventsDisposable.set(nil) + self.qrErrorRetryDisposable.set(nil) + self.qrErrorRetryAttempt = 0 + } + private func refreshQrToken() { guard let account = self.account else { return } let sharedContext = self.sharedContext - let tokenSignal = sharedContext.activeAccountContexts - |> castError(ExportAuthTransferTokenError.self) - |> take(1) - |> mapToSignal { activeAccountsAndInfo -> Signal in - let (_, activeAccounts, _) = activeAccountsAndInfo - let activeProductionUserIds = activeAccounts.map({ $0.1.account }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) - let activeTestingUserIds = activeAccounts.map({ $0.1.account }).filter({ $0.testingEnvironment }).map({ $0.peerId.id }) - - let allProductionUserIds = activeProductionUserIds - let allTestingUserIds = activeTestingUserIds - - return TelegramEngineUnauthorized(account: account).auth.exportAuthTransferToken(accountManager: sharedContext.accountManager, otherAccountUserIds: account.testingEnvironment ? allTestingUserIds : allProductionUserIds, syncContacts: true) - } - - self.exportTokenDisposable.set((tokenSignal + // MARK: Swiftgram + self.exportTokenDisposable.set((sgExportQrLoginToken(account: account, sharedContext: sharedContext) |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } + strongSelf.qrErrorRetryAttempt = 0 switch result { case let .displayToken(token): var tokenString = token.value.base64EncodedString() - //print("export token \(tokenString)") tokenString = tokenString.replacingOccurrences(of: "+", with: "-") tokenString = tokenString.replacingOccurrences(of: "/", with: "_") let urlString = "tg://login?token=\(tokenString)" - let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none) - |> deliverOnMainQueue).startStandalone(next: { _, generate in - guard let strongSelf = self else { - return - } - - let context = generate(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 200.0, height: 200.0), boundingSize: CGSize(width: 200.0, height: 200.0), intrinsicInsets: UIEdgeInsets())) - if let image = context?.generateImage() { - strongSelf.qrNode?.image = image - } - }) - + + // MARK: Swiftgram + if let presentedQrCodeScreen = strongSelf.presentedQrCodeScreen { + presentedQrCodeScreen.updateSubject(.loginToken(url: urlString)) + } else { + strongSelf.presentedQrCodeScreen = strongSelf.presentQrCode?(urlString) ?? nil + } + + // MARK: Swiftgram + strongSelf.tokenEventsDisposable.set((account.updateLoginTokenEvents + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] _ in + strongSelf?.refreshQrToken() + })) + let timestamp = Int32(Date().timeIntervalSince1970) let timeout = max(5, token.validUntil - timestamp) - strongSelf.exportTokenDisposable.set((Signal.complete() - |> delay(Double(timeout), queue: .mainQueue())).startStrict(completed: { - guard let strongSelf = self else { + strongSelf.qrRefreshTimerDisposable.set((Signal.complete() + |> delay(Double(timeout), queue: .mainQueue())).startStrict(completed: { [weak strongSelf] in + guard let strongSelf, strongSelf.presentedQrCodeScreen != nil else { return } strongSelf.refreshQrToken() @@ -772,9 +768,42 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self?.refreshQrToken() })) strongSelf.refreshQrToken() - case .loggedIn, .passwordRequested: - strongSelf.exportTokenDisposable.set(nil) + case .loggedIn: + strongSelf.stopQrLoop() + case let .passwordRequested(account): + // MARK: Swiftgram + strongSelf.stopQrLoop() + strongSelf.account = account + strongSelf.accountUpdated?(account) } + }, error: { [weak self] error in + guard let strongSelf = self else { + return + } + + // MARK: Swiftgram + switch error { + case .authKeyUnregistered, .authTokenExpired: + if strongSelf.presentedQrCodeScreen != nil { + let attempt = strongSelf.qrErrorRetryAttempt + strongSelf.qrErrorRetryAttempt += 1 + let currentDelay = min(6.0, 1.5 + Double(attempt) * 1.0) + strongSelf.qrErrorRetryDisposable.set((Signal.complete() + |> delay(currentDelay, queue: .mainQueue())).startStrict(completed: { [weak strongSelf] in + strongSelf?.refreshQrToken() + })) + return + } + strongSelf.stopQrLoop() + return + case .generic, .limitExceeded: + break + } + + strongSelf.stopQrLoop() + strongSelf.presentedQrCodeScreen?.dismissAnimated() + strongSelf.presentedQrCodeScreen = nil + strongSelf.qrLoginError?(error) })) } } diff --git a/submodules/PassportUI/Sources/SecureIdAuthController.swift b/submodules/PassportUI/Sources/SecureIdAuthController.swift index ce312d33d69..f9b17b5db5a 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthController.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthController.swift @@ -483,7 +483,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable errorText = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError case .limitExceeded: errorText = strongSelf.presentationData.strings.LoginPassword_FloodError - case .generic: + case .generic, .authKeyUnregistered: errorText = strongSelf.presentationData.strings.Login_UnknownError } case .generic: diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index aea80e2cf46..574ffa51701 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -443,7 +443,7 @@ public final class TwoFactorDataInputScreen: ViewController { text = presentationData.strings.LoginPassword_FloodError case .invalidPassword: text = nil - case .generic: + case .generic, .authKeyUnregistered: text = presentationData.strings.Login_UnknownError } if let text = text { diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index fd01c51c1ae..38251794b51 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -70,6 +70,10 @@ private final class SheetContent: CombinedComponent { if lhs.sharedContext !== rhs.sharedContext { return false } + // MARK: Swiftgram + if lhs.subject != rhs.subject { + return false + } return true } @@ -181,6 +185,9 @@ private final class SheetContent: CombinedComponent { case .proxy: titleString = "" textString = strings.SocksProxySetup_ShareQRCodeInfo + case .loginToken: + titleString = strings.AuthSessions_AddDeviceIntro_Title + textString = strings.AuthSessions_AddDevice_UrlLoginHint default: titleString = "" textString = "" @@ -313,33 +320,35 @@ private final class SheetContent: CombinedComponent { contentSize.height += 23.0 let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) - let button = button.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) - ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.InviteLink_QRCode_Share, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak controller] in - if let view = controller?.view { - shareQrCode(sharedContext: component.sharedContext, subject: effectiveSubject, asImage: true, view: view) + if component.subject.showsShareButton { + let button = button.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) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.InviteLink_QRCode_Share, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller] in + if let view = controller?.view { + shareQrCode(sharedContext: component.sharedContext, subject: effectiveSubject, asImage: true, view: view) + } } - } - ), - availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), - transition: .immediate - ) - context.add(button - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) - ) - contentSize.height += button.size.height + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + } if case .proxy = component.subject { contentSize.height += 8.0 @@ -399,6 +408,10 @@ private final class QrCodeSheetComponent: CombinedComponent { if lhs.sharedContext !== rhs.sharedContext { return false } + // MARK: Swiftgram + if lhs.subject != rhs.subject { + return false + } return true } @@ -479,7 +492,9 @@ public final class QrCodeScreen: ViewControllerComponentContainer { case invite(invite: ExportedInvitation, type: SubjectType) case chatFolder(slug: String) case proxy(server: ProxyServerSettings, externalLink: Bool) - + // MARK: Swiftgram + case loginToken(url: String) + var link: String { switch self { case let .peer(peer): @@ -510,31 +525,48 @@ public final class QrCodeScreen: ViewControllerComponentContainer { } } return link + // MARK: Swiftgram + case let .loginToken(url): + return url } } - + var ecl: String { switch self { - case .peer, .invite, .chatFolder, .proxy: + case .peer, .invite, .chatFolder, .proxy, .loginToken: return "Q" } } var icon: QrCodeIcon { switch self { - case .peer, .invite, .chatFolder: + case .peer, .invite, .chatFolder, .loginToken: return .custom(UIImage(bundleImageName: "Chat/Links/QrLogo")) case .proxy: return .proxy } } + + // MARK: Swiftgram + var showsShareButton: Bool { + switch self { + case .loginToken: + return false + case .peer, .invite, .chatFolder, .proxy: + return true + } + } } + // MARK: Swiftgram + private let sgSharedContext: SharedAccountContext + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: QrCodeScreen.Subject ) { + self.sgSharedContext = context.sharedContext super.init( context: context, component: QrCodeSheetComponent( @@ -546,15 +578,16 @@ public final class QrCodeScreen: ViewControllerComponentContainer { theme: .default, updatedPresentationData: updatedPresentationData ) - + self.navigationPresentation = .flatModal } - + public init( sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), subject: QrCodeScreen.Subject - ) { + ) { + self.sgSharedContext = sharedContext super.init( component: QrCodeSheetComponent( sharedContext: sharedContext, @@ -579,6 +612,26 @@ public final class QrCodeScreen: ViewControllerComponentContainer { view.dismissAnimated() } } + + // MARK: Swiftgram + // Refreshes the displayed code in place (e.g. a new .loginToken url after the previous one + // expired) instead of pushing a second sheet on top of this one. + public func updateSubject(_ subject: QrCodeScreen.Subject) { + self.updateComponent(component: AnyComponent(QrCodeSheetComponent( + sharedContext: self.sgSharedContext, + subject: subject + )), transition: .easeInOut(duration: 0.2)) + } + + // MARK: Swiftgram + public var didDismiss: (() -> Void)? + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + if let navigationController = self.navigationController, navigationController.viewControllers.contains(where: { $0 === self }) { + return + } + self.didDismiss?() + } } private final class QrCodeComponent: Component { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index 3a3f2e80534..b88f74880b7 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -474,10 +474,10 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, text = presentationData.strings.LoginPassword_FloodError case .invalidPassword: text = presentationData.strings.LoginPassword_InvalidPasswordError - case .generic: + case .generic, .authKeyUnregistered: text = presentationData.strings.Login_UnknownError } - + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 61c1d419553..2f3006bae6b 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -1142,6 +1142,8 @@ public func beginSignUp(account: UnauthorizedAccount, data: AuthorizationSignUpD public enum AuthorizationPasswordVerificationError { case limitExceeded case invalidPassword + // MARK: Swiftgram + case authKeyUnregistered case generic } @@ -1152,6 +1154,8 @@ public func authorizeWithPassword(accountManager: AccountManager castError(ExportAuthTransferTokenError.self) } } + } else if error.errorDescription == "AUTH_KEY_UNREGISTERED" { + return .fail(.authKeyUnregistered) + } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .fail(.limitExceeded) } else { return .fail(.generic) } @@ -85,6 +92,12 @@ func _internal_exportAuthTransferToken(accountManager: AccountManager castError(ExportAuthTransferTokenError.self) } } + } else if error.errorDescription == "AUTH_KEY_UNREGISTERED" { + return .fail(.authKeyUnregistered) + } else if error.errorDescription == "AUTH_TOKEN_EXPIRED" { + return .fail(.authTokenExpired) + } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .fail(.limitExceeded) } else { return .fail(.generic) }