Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Swiftgram/SGQrLogin/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
117 changes: 117 additions & 0 deletions Swiftgram/SGQrLogin/Sources/SGQrLogin.swift
Original file line number Diff line number Diff line change
@@ -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<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> {
return sharedContext.activeAccountContexts
|> castError(ExportAuthTransferTokenError.self)
|> take(1)
|> mapToSignal { activeAccountsAndInfo -> Signal<ExportAuthTransferTokenResult, ExportAuthTransferTokenError> 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<Void, AuthorizationPasswordVerificationError> {
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<Never, NoError>.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
)
}
1 change: 1 addition & 0 deletions Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
5 changes: 3 additions & 2 deletions submodules/AuthorizationUI/BUILD
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import AlertUI
import InAppPurchaseManager
import ObjectiveC
import AVFoundation
import SGQrLogin

private var ObjCKey_Delegate: Int?

Expand Down Expand Up @@ -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:
Expand All @@ -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()
}
}
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
Loading