Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin
2025-12-02 12:36:13 +04:00
25 changed files with 313 additions and 215 deletions

View File

@@ -496,6 +496,7 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id
<string>applinks:t.me</string>
<string>applinks:*.t.me</string>
<string>webcredentials:t.me</string>
<string>webcredentials:telegram.org</string>
</array>
"""

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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):

View File

@@ -340,7 +340,13 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
}
}
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: 2, keychain: keychain, basePath: path, testingEnvironment: beginWithTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
#if DEBUG
let initialDatacenterId: Int = 1
#else
let initialDatacenterId: Int = 2
#endif
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: initialDatacenterId, keychain: keychain, basePath: path, testingEnvironment: beginWithTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
|> 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))
}

View File

@@ -1183,91 +1183,117 @@ public final class AuthorizationPasskeyData {
}
}
public func authorizeWithPasskey(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> {
return account.postbox.transaction { transaction -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, foreignDatacenter: (id: Int, authKeyId: Int64)?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal<AuthorizeWithPasskeyResult, AuthorizationCodeVerificationError> {
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<AuthorizeWithPasskeyResult, AuthorizationCodeVerificationError> in
let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: Int32(targetDatacenterId))
return updatedAccount
|> mapToSignalPromotingError { updatedAccount -> Signal<AuthorizeWithPasskeyResult, AuthorizationCodeVerificationError> in
return authorizeWithPasskey(accountManager: accountManager, account: updatedAccount, passkey: passkey, foreignDatacenter: (Int(initialDatacenterId), sourceAuthKeyId), forcedPasswordSetupNotice: forcedPasswordSetupNotice, syncContacts: syncContacts)
}
}
|> mapToSignal { result -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> in
return account.postbox.transaction { transaction -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<AuthorizeWithPasskeyResult, AuthorizationCodeVerificationError> in
return account.postbox.transaction { transaction -> Signal<AuthorizeWithPasskeyResult, AuthorizationCodeVerificationError> 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 {

View File

@@ -1065,6 +1065,22 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
public func getAuthKeyId() -> Signal<Int64, NoError> {
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<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<NetworkRequestResult<T>, MTRpcError> {
let requestService = self.requestService
return Signal { subscriber in

View File

@@ -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",

View File

@@ -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)")
}

View File

@@ -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."
)

View File

@@ -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<Empty>
if let emojiId = passkey.emojiId {
iconComponent = AnyComponentWithIdentity(
id: "lottie",
component: AnyComponent(TransformContents<Empty>(
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,

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "key_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Icon-5.pdf",
"filename" : "lock_30.pdf",
"idiom" : "universal"
}
],

View File

@@ -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

View File

@@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "key.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "faceid.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "lock.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}