diff --git a/Telegram/BUILD b/Telegram/BUILD
index aacae96803..74b3ecae94 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -496,6 +496,7 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id
applinks:t.me
applinks:*.t.me
webcredentials:t.me
+ webcredentials:telegram.org
"""
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
index 9c9c03a5d3..b6d861d6f2 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
@@ -324,6 +324,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
accountManager: self.sharedContext.accountManager,
account: self.account,
passkey: passkey,
+ foreignDatacenter: nil,
forcedPasswordSetupNotice: { value in
guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else {
return nil
@@ -332,7 +333,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
},
syncContacts: syncContacts
)
- |> deliverOnMainQueue).startStrict(next: { _ in
+ |> deliverOnMainQueue).startStrict(next: { [weak self] result in
+ guard let self else {
+ return
+ }
+ if result.updatedAccount !== self.account {
+ self.account = result.updatedAccount
+ self.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(self.engine))
+ }
}, error: { [weak self, weak controller] error in
Queue.mainQueue().async {
if let strongSelf = self, let controller {
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
index ac22728099..54855476a3 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
@@ -157,6 +157,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
strongSelf.account = account
strongSelf.accountUpdated?(account)
}
+ self.controllerNode.retryPasskey = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.loadAndPresentPasskey(force: true)
+ }
if let (code, name, number) = self.currentData {
self.controllerNode.codeAndNumber = (code, name, number)
@@ -194,7 +200,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
self.controllerNode.updateCountryCode()
}
- if #available(iOS 15.0, *) {
+ self.loadAndPresentPasskey(force: false)
+ }
+
+ private func loadAndPresentPasskey(force: Bool) {
+ if #available(iOS 16.0, *) {
Task { @MainActor [weak self] in
guard let self, let account = self.account else {
return
@@ -235,7 +245,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
authController.delegate = self
authController.presentationContextProvider = self
- authController.performRequests()
+ if force {
+ authController.performRequests()
+ } else {
+ authController.performRequests(options: [.preferImmediatelyAvailableCredentials])
+ }
}
}
}
@@ -290,6 +304,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
}
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
+ if (error as NSError).domain == "com.apple.AuthenticationServices.AuthorizationError" && (error as NSError).code == 1001 {
+ self.controllerNode.updateDisplayPasskeyLoginOption()
+ if let validLayout = self.validLayout {
+ self.containerLayoutUpdated(validLayout, transition: .immediate)
+ }
+ }
}
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
index fafaac93ba..189f8e7c13 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
@@ -15,6 +15,7 @@ import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
import AuthorizationUtils
import ManagedAnimationNode
+import Markdown
private final class PhoneAndCountryNode: ASDisplayNode {
let strings: PresentationStrings
@@ -312,7 +313,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let managedAnimationNode: ManagedPhoneAnimationNode
private let titleNode: ASTextNode
private let titleActivateAreaNode: AccessibilityAreaNode
- private let noticeNode: ASTextNode
+ private let noticeNode: ImmediateTextNode
private let noticeActivateAreaNode: AccessibilityAreaNode
private let phoneAndCountryNode: PhoneAndCountryNode
private let contactSyncNode: ContactSyncNode
@@ -323,6 +324,8 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let tokenEventsDisposable = MetaDisposable()
var accountUpdated: ((UnauthorizedAccount) -> Void)?
+ var retryPasskey: (() -> Void)?
+
private let debugAction: () -> Void
var currentNumber: String {
@@ -405,7 +408,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.titleActivateAreaNode = AccessibilityAreaNode()
self.titleActivateAreaNode.accessibilityTraits = .staticText
- self.noticeNode = ASTextNode()
+ self.noticeNode = ImmediateTextNode()
self.noticeNode.maximumNumberOfLines = 0
self.noticeNode.isUserInteractionEnabled = true
self.noticeNode.displaysAsynchronously = false
@@ -443,6 +446,23 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.addSubnode(self.managedAnimationNode)
self.contactSyncNode.isHidden = true
+ self.noticeNode.highlightAttributeAction = { attributes in
+ if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
+ return NSAttributedString.Key(rawValue: "URL")
+ } else {
+ return nil
+ }
+ }
+ self.noticeNode.tapAttributeAction = { [weak self] attributes, _ in
+ guard let self else {
+ return
+ }
+ if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
+ self.retryPasskey?()
+ }
+ }
+ self.noticeNode.linkHighlightColor = theme.list.itemAccentColor.withAlphaComponent(0.2)
+
self.phoneAndCountryNode.selectCountryCode = { [weak self] in
self?.selectCountryCode?()
}
@@ -484,7 +504,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
super.didLoad()
self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:))))
- #if DEBUG
+ #if DEBUG && false
self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:))))
#endif
}
@@ -555,6 +575,28 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
let _ = self.phoneAndCountryNode.processNumberChange(number: self.phoneAndCountryNode.phoneInputNode.number)
}
+ func updateDisplayPasskeyLoginOption() {
+ //TODO:localize
+ if self.account == nil {
+ return
+ }
+ let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Enter your phone number\nor [log in using Passkey >](passkey)", attributes: MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(17.0), textColor: self.theme.list.itemPrimaryTextColor),
+ link: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor),
+ linkAttribute: { url in
+ return ("URL", url)
+ }
+ )))
+ let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: self.theme.list.itemAccentColor)
+
+ if let range = attributedText.string.range(of: ">"), let chevronImage {
+ attributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedText.string))
+ }
+
+ self.noticeNode.attributedText = attributedText
+ }
+
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [])
insets.top = layout.statusBarHeight ?? 20.0
@@ -576,7 +618,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
let noticeInset: CGFloat = self.account == nil ? 32.0 : 0.0
- let noticeSize = self.noticeNode.measure(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude))
+ let noticeSize = self.noticeNode.updateLayout(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude))
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - inset * 2.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight)
diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
index ee04c6c975..c7ec6407bb 100644
--- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
+++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
@@ -501,7 +501,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
arguments.openTwoStepVerification(data)
})
case let .passkeys(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Passkeys")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openPasskeys()
})
case let .messageAutoremoveTimeout(_, text, value):
diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift
index 076f486ed2..262176da07 100644
--- a/submodules/TelegramCore/Sources/Account/Account.swift
+++ b/submodules/TelegramCore/Sources/Account/Account.swift
@@ -340,7 +340,13 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in
return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection))
}
diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift
index 7d9c14bf2e..db7ce4dd78 100644
--- a/submodules/TelegramCore/Sources/Authorization.swift
+++ b/submodules/TelegramCore/Sources/Authorization.swift
@@ -1183,91 +1183,117 @@ public final class AuthorizationPasskeyData {
}
}
-public func authorizeWithPasskey(accountManager: AccountManager, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal {
- return account.postbox.transaction { transaction -> Signal in
- return account.network.request(Api.functions.auth.finishPasskeyLogin(flags: 0, credential: .inputPasskeyCredentialPublicKey(id: passkey.id, rawId: passkey.id, response: .inputPasskeyResponseLogin(clientData: .dataJSON(data: passkey.clientData), authenticatorData: Buffer(data: passkey.authenticatorData), signature: Buffer(data: passkey.signature), userHandle: passkey.userHandle)), fromDcId: nil, fromAuthKeyId: nil), automaticFloodWait: false)
- |> map { authorization in
- return .authorization(authorization)
- }
- |> `catch` { error -> Signal in
- switch (error.errorCode, error.errorDescription ?? "") {
- case (401, "SESSION_PASSWORD_NEEDED"):
- return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
- |> mapError { error -> AuthorizationCodeVerificationError in
- if error.errorDescription.hasPrefix("FLOOD_WAIT") {
- return .limitExceeded
- } else {
- return .generic
- }
- }
- |> mapToSignal { result -> Signal in
- switch result {
- case let .password(_, _, _, _, hint, _, _, _, _, _, _):
- return .single(.password(hint: hint ?? ""))
- }
- }
- case let (_, errorDescription):
- if errorDescription.hasPrefix("FLOOD_WAIT") {
- return .fail(.limitExceeded)
- } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" {
- return .fail(.invalidCode)
- } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" {
- return .fail(.codeExpired)
- } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" {
- return .single(.signUp)
- } else if errorDescription == "EMAIL_TOKEN_INVALID" {
- return .fail(.invalidEmailToken)
- } else if errorDescription == "EMAIL_ADDRESS_INVALID" {
- return .fail(.invalidEmailAddress)
- } else {
- return .fail(.generic)
- }
+public final class AuthorizeWithPasskeyResult {
+ public let updatedAccount: UnauthorizedAccount
+
+ init(updatedAccount: UnauthorizedAccount) {
+ self.updatedAccount = updatedAccount
+ }
+}
+
+public func authorizeWithPasskey(accountManager: AccountManager, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, foreignDatacenter: (id: Int, authKeyId: Int64)?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal {
+ let userHandle = passkey.userHandle.components(separatedBy: ":")
+ var targetDatacenterId: Int?
+ if foreignDatacenter == nil && userHandle.count >= 2 {
+ targetDatacenterId = Int(userHandle[0])
+ }
+
+ if let targetDatacenterId, account.masterDatacenterId != Int32(targetDatacenterId) {
+ let initialDatacenterId = account.masterDatacenterId
+ return account.network.getAuthKeyId()
+ |> castError(AuthorizationCodeVerificationError.self)
+ |> mapToSignal { sourceAuthKeyId -> Signal in
+ let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: Int32(targetDatacenterId))
+ return updatedAccount
+ |> mapToSignalPromotingError { updatedAccount -> Signal in
+ return authorizeWithPasskey(accountManager: accountManager, account: updatedAccount, passkey: passkey, foreignDatacenter: (Int(initialDatacenterId), sourceAuthKeyId), forcedPasswordSetupNotice: forcedPasswordSetupNotice, syncContacts: syncContacts)
}
}
- |> mapToSignal { result -> Signal in
- return account.postbox.transaction { transaction -> Signal in
- switch result {
- case .signUp:
- return .fail(.generic)
- case let .password(hint):
- transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: nil, code: nil, suggestReset: false, syncContacts: syncContacts)))
- return .single(.loggedIn)
- case let .authorization(authorization):
- switch authorization {
- case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user):
- if let futureAuthToken = futureAuthToken {
- storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
- }
-
- let user = TelegramUser(user: user)
- var isSupportUser = false
- if let phone = user.phone, phone.hasPrefix("42") {
- isSupportUser = true
- }
- let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
- initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
- transaction.setState(state)
- if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
- transaction.setNoticeEntry(key: value.0, value: value.1)
- }
- return accountManager.transaction { transaction -> AuthorizeWithCodeResult in
- switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
- return .loggedIn
- }
- |> castError(AuthorizationCodeVerificationError.self)
- case .authorizationSignUpRequired:
- return .fail(.generic)
+ }
+
+ var flags: Int32 = 0
+ if foreignDatacenter != nil {
+ flags |= 1 << 0
+ }
+ return account.network.request(Api.functions.auth.finishPasskeyLogin(flags: flags, credential: .inputPasskeyCredentialPublicKey(id: passkey.id, rawId: passkey.id, response: .inputPasskeyResponseLogin(clientData: .dataJSON(data: passkey.clientData), authenticatorData: Buffer(data: passkey.authenticatorData), signature: Buffer(data: passkey.signature), userHandle: passkey.userHandle)), fromDcId: (foreignDatacenter?.id).flatMap(Int32.init), fromAuthKeyId: foreignDatacenter?.authKeyId), automaticFloodWait: false)
+ |> map { authorization in
+ return .authorization(authorization)
+ }
+ |> `catch` { error -> Signal in
+ switch (error.errorCode, error.errorDescription ?? "") {
+ case (401, "SESSION_PASSWORD_NEEDED"):
+ return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
+ |> mapError { error -> AuthorizationCodeVerificationError in
+ if error.errorDescription.hasPrefix("FLOOD_WAIT") {
+ return .limitExceeded
+ } else {
+ return .generic
}
}
- }
- |> mapError { _ -> AuthorizationCodeVerificationError in
- }
- |> switchToLatest
+ |> mapToSignal { result -> Signal in
+ switch result {
+ case let .password(_, _, _, _, hint, _, _, _, _, _, _):
+ return .single(.password(hint: hint ?? ""))
+ }
+ }
+ case let (_, errorDescription):
+ if errorDescription.hasPrefix("FLOOD_WAIT") {
+ return .fail(.limitExceeded)
+ } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" {
+ return .fail(.invalidCode)
+ } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" {
+ return .fail(.codeExpired)
+ } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" {
+ return .single(.signUp)
+ } else if errorDescription == "EMAIL_TOKEN_INVALID" {
+ return .fail(.invalidEmailToken)
+ } else if errorDescription == "EMAIL_ADDRESS_INVALID" {
+ return .fail(.invalidEmailAddress)
+ } else {
+ return .fail(.generic)
+ }
}
}
- |> mapError { _ -> AuthorizationCodeVerificationError in
+ |> mapToSignal { result -> Signal in
+ return account.postbox.transaction { transaction -> Signal in
+ switch result {
+ case .signUp:
+ return .fail(.generic)
+ case let .password(hint):
+ transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: nil, code: nil, suggestReset: false, syncContacts: syncContacts)))
+ return .single(AuthorizeWithPasskeyResult(updatedAccount: account))
+ case let .authorization(authorization):
+ switch authorization {
+ case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user):
+ if let futureAuthToken = futureAuthToken {
+ storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
+ }
+
+ let user = TelegramUser(user: user)
+ var isSupportUser = false
+ if let phone = user.phone, phone.hasPrefix("42") {
+ isSupportUser = true
+ }
+ let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
+ initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
+ transaction.setState(state)
+ if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
+ transaction.setNoticeEntry(key: value.0, value: value.1)
+ }
+ return accountManager.transaction { transaction -> AuthorizeWithPasskeyResult in
+ switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
+ return AuthorizeWithPasskeyResult(updatedAccount: account)
+ }
+ |> castError(AuthorizationCodeVerificationError.self)
+ case .authorizationSignUpRequired:
+ return .fail(.generic)
+ }
+ }
+ }
+ |> mapError { _ -> AuthorizationCodeVerificationError in
+ }
+ |> switchToLatest
}
- |> switchToLatest
}
public enum PasswordRecoveryRequestError {
diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift
index f9dd1d6c1e..75e53aa7b6 100644
--- a/submodules/TelegramCore/Sources/Network/Network.swift
+++ b/submodules/TelegramCore/Sources/Network/Network.swift
@@ -1065,6 +1065,22 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
+ public func getAuthKeyId() -> Signal {
+ let mtContext = self.mtProto.context
+ let datacenterId = self.datacenterId
+ return Signal { subscriber in
+ MTContext.contextQueue().dispatch(onQueue: {
+ var result: Int64 = 0
+ if let authInfo = mtContext?.authInfoForDatacenter(withId: datacenterId, selector: .persistent) {
+ result = authInfo.authKeyId
+ }
+ subscriber.putNext(result)
+ })
+
+ return EmptyDisposable
+ }
+ }
+
public func requestWithAdditionalInfo(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal, MTRpcError> {
let requestService = self.requestService
return Signal { subscriber in
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD
index 0c4a7d0c38..38a7d38057 100644
--- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD
@@ -29,6 +29,7 @@ swift_library(
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
+ "//submodules/TelegramUI/Components/EmojiStatusComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift
index c5fd773915..bedfb253ea 100644
--- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift
@@ -169,12 +169,12 @@ final class PasskeysScreenComponent: Component {
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
//TODO:localize
- controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Passkey?", text: "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.", actions: [TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in
+ controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Passkey?", text: "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
+ }), TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in
guard let self else {
return
}
self.deletePasskey(id: id)
- }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})]), in: .window(.root))
}
@@ -204,7 +204,7 @@ final class PasskeysScreenComponent: Component {
}
if let credentialId = decodeBase64(passkey.id) {
do {
- try await updater.reportUnknownPublicKeyCredential(relyingPartyIdentifier: "t.me", credentialID: credentialId)
+ try await updater.reportUnknownPublicKeyCredential(relyingPartyIdentifier: "telegram.org", credentialID: credentialId)
} catch let e {
Logger.shared.log("Passkeys", "reportUnknownPublicKeyCredential error: \(e)")
}
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift
index e87589c9fc..28df5748a5 100644
--- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift
@@ -114,7 +114,7 @@ final class PasskeysScreenIntroComponent: Component {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
- content: LottieComponent.AppBundleContent(name: "TwoFactorSetupIntro"),
+ content: LottieComponent.AppBundleContent(name: "passkey_logo"),
loop: false
)),
environment: {},
@@ -182,17 +182,17 @@ final class PasskeysScreenIntroComponent: Component {
}
let itemDescs: [ItemDesc] = [
ItemDesc(
- icon: "Chat List/Archive/IconArchived",
+ icon: "Settings/Passkeys/Intro1",
title: "Create a Passkey",
text: "Make a passkey to sign in easily and safely."
),
ItemDesc(
- icon: "Chat List/Archive/IconHide",
+ icon: "Settings/Passkeys/Intro2",
title: "Log in with Face ID",
text: "Use Face ID, Touch ID, or your passcode to sign in."
),
ItemDesc(
- icon: "Chat List/Archive/IconStories",
+ icon: "Settings/Passkeys/Intro3",
title: "Store Passkey Securely",
text: "Your passkey is safely kept in your iCloud Keychain."
)
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift
index 4cd18eedd6..dfafe38578 100644
--- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift
@@ -12,6 +12,7 @@ import BundleIconComponent
import ListSectionComponent
import ListActionItemComponent
import TelegramCore
+import EmojiStatusComponent
final class PasskeysScreenListComponent: Component {
let context: AccountContext
@@ -112,7 +113,7 @@ final class PasskeysScreenListComponent: Component {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
- content: LottieComponent.AppBundleContent(name: "TwoFactorSetupIntro"),
+ content: LottieComponent.AppBundleContent(name: "passkey_logo"),
loop: false
)),
environment: {},
@@ -184,6 +185,43 @@ final class PasskeysScreenListComponent: Component {
dateFormatter.dateStyle = .medium
let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(passkey.date)))
+ let iconComponent: AnyComponentWithIdentity
+ if let emojiId = passkey.emojiId {
+ iconComponent = AnyComponentWithIdentity(
+ id: "lottie",
+ component: AnyComponent(TransformContents(
+ content: AnyComponent(EmojiStatusComponent(
+ context: component.context,
+ animationCache: component.context.animationCache,
+ animationRenderer: component.context.animationRenderer,
+ content: .animation(
+ content: .customEmoji(fileId: emojiId),
+ size: CGSize(width: 40.0, height: 40.0),
+ placeholderColor: component.theme.list.mediaPlaceholderColor,
+ themeColor: nil,
+ loopMode: .count(1)
+ ),
+ size: CGSize(width: 40.0, height: 40.0),
+ isVisibleForAnimations: true,
+ action: nil
+ )),
+ translation: CGPoint(x: 0.0, y: 1.0)
+ ))
+ )
+ } else {
+ iconComponent = AnyComponentWithIdentity(
+ id: "icon",
+ component: AnyComponent(BundleIconComponent(name: "Settings/Menu/Passkeys", tintColor: nil))
+ )
+ }
+
+ //TODO:localize
+ var subtitleString = "created \(dateString)"
+ if let lastUsageDate = passkey.lastUsageDate {
+ let lastUsedDateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(lastUsageDate)))
+ subtitleString.append(" • used \(lastUsedDateString)")
+ }
+
listSectionItems.append(AnyComponentWithIdentity(id: passkey.id, component: AnyComponent(ListActionItemComponent(
theme: component.theme,
title: AnyComponent(VStack([
@@ -197,7 +235,7 @@ final class PasskeysScreenListComponent: Component {
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
- string: "created \(dateString)",
+ string: subtitleString,
font: Font.regular(14.0),
textColor: component.theme.list.itemSecondaryTextColor
)),
@@ -205,10 +243,7 @@ final class PasskeysScreenListComponent: Component {
)))
], alignment: .left, spacing: 2.0)),
leftIcon: .custom(
- AnyComponentWithIdentity(
- id: "icon",
- component: AnyComponent(BundleIconComponent(name: "Settings/Menu/TwoStepAuth", tintColor: nil))
- ),
+ iconComponent,
false
),
accessory: nil,
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json
new file mode 100644
index 0000000000..be5de9e9a0
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "key_30.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf
new file mode 100644
index 0000000000..301727da15
Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Contents.json
index 364c182b89..e222b7cee1 100644
--- a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Contents.json
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Contents.json
@@ -1,7 +1,7 @@
{
"images" : [
{
- "filename" : "Icon-5.pdf",
+ "filename" : "lock_30.pdf",
"idiom" : "universal"
}
],
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Icon-5.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Icon-5.pdf
deleted file mode 100644
index 0d7b1513ed..0000000000
--- a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Icon-5.pdf
+++ /dev/null
@@ -1,114 +0,0 @@
-%PDF-1.7
-
-1 0 obj
- << >>
-endobj
-
-2 0 obj
- << /Length 3 0 R >>
-stream
-/DeviceRGB CS
-/DeviceRGB cs
-q
-1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
-1.000000 0.584314 0.000000 scn
-0.000000 18.799999 m
-0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
-1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
-5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
-18.799999 30.000000 l
-22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
-27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
-30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
-30.000000 11.200001 l
-30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
-28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
-24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
-11.200000 0.000000 l
-7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
-2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
-0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
-0.000000 18.799999 l
-h
-f
-n
-Q
-q
--1.000000 -0.000000 -0.000000 1.000000 25.000000 6.000000 cm
-1.000000 1.000000 1.000000 scn
-6.650000 19.000000 m
-2.945000 19.000000 0.000000 16.055000 0.000000 12.350000 c
-0.000000 8.645000 2.945000 5.700001 6.650000 5.700001 c
-7.500165 5.700001 8.310955 5.861581 9.054688 6.145313 c
-10.450001 4.750000 l
-12.350000 4.750000 l
-12.350000 2.850000 l
-14.250000 2.850000 l
-14.250000 0.950001 l
-14.903126 0.296875 l
-15.093125 0.106874 15.300938 0.000000 15.585938 0.000000 c
-18.049999 0.000000 l
-18.619999 0.000000 19.000000 0.380001 19.000000 0.950001 c
-19.000000 3.414062 l
-19.000000 3.699062 18.893126 3.906875 18.703125 4.096874 c
-12.854687 9.945312 l
-13.138419 10.689045 13.299999 11.499835 13.299999 12.350000 c
-13.299999 16.055000 10.355000 19.000000 6.650000 19.000000 c
-h
-5.225000 16.150000 m
-6.555000 16.150000 7.600000 15.105000 7.600000 13.775000 c
-7.600000 12.445000 6.555000 11.400000 5.225000 11.400000 c
-3.895000 11.400000 2.850000 12.445000 2.850000 13.775000 c
-2.850000 15.105000 3.895000 16.150000 5.225000 16.150000 c
-h
-f*
-n
-Q
-
-endstream
-endobj
-
-3 0 obj
- 1987
-endobj
-
-4 0 obj
- << /Annots []
- /Type /Page
- /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
- /Resources 1 0 R
- /Contents 2 0 R
- /Parent 5 0 R
- >>
-endobj
-
-5 0 obj
- << /Kids [ 4 0 R ]
- /Count 1
- /Type /Pages
- >>
-endobj
-
-6 0 obj
- << /Type /Catalog
- /Pages 5 0 R
- >>
-endobj
-
-xref
-0 7
-0000000000 65535 f
-0000000010 00000 n
-0000000034 00000 n
-0000002077 00000 n
-0000002100 00000 n
-0000002273 00000 n
-0000002347 00000 n
-trailer
-<< /ID [ (some) (id) ]
- /Root 6 0 R
- /Size 7
->>
-startxref
-2406
-%%EOF
\ No newline at end of file
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf
new file mode 100644
index 0000000000..b1f6efd7ab
Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json
new file mode 100644
index 0000000000..6e965652df
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json
new file mode 100644
index 0000000000..7ee24619ad
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "key.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf
new file mode 100644
index 0000000000..29928ca1db
Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/Contents.json
new file mode 100644
index 0000000000..75ffa7e156
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "faceid.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/faceid.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/faceid.pdf
new file mode 100644
index 0000000000..ac9b51c120
Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/faceid.pdf differ
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/Contents.json
new file mode 100644
index 0000000000..c6b56e0023
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "lock.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/lock.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/lock.pdf
new file mode 100644
index 0000000000..8001b33c64
Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/lock.pdf differ
diff --git a/submodules/TelegramUI/Resources/Animations/passkey_logo.tgs b/submodules/TelegramUI/Resources/Animations/passkey_logo.tgs
new file mode 100644
index 0000000000..62e11b2540
Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/passkey_logo.tgs differ