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