import Foundation import UIKit import Display import ComponentFlow import TelegramPresentationData import AccountContext import LottieComponent import MultilineTextComponent import BalancedTextComponent import ButtonComponent import BundleIconComponent final class PasskeysScreenIntroComponent: Component { let context: AccountContext let theme: PresentationTheme let insets: UIEdgeInsets let displaySkip: Bool let createPasskeyAction: () -> Void init( context: AccountContext, theme: PresentationTheme, insets: UIEdgeInsets, displaySkip: Bool, createPasskeyAction: @escaping () -> Void ) { self.context = context self.theme = theme self.insets = insets self.displaySkip = displaySkip self.createPasskeyAction = createPasskeyAction } static func ==(lhs: PasskeysScreenIntroComponent, rhs: PasskeysScreenIntroComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.insets != rhs.insets { return false } if lhs.displaySkip != rhs.displaySkip { return false } return true } private final class ScrollViewImpl: UIScrollView { override func touchesShouldCancel(in view: UIView) -> Bool { return true } } private final class Item { let icon = ComponentView() let title = ComponentView() let text = ComponentView() init() { } } class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl private let contentContainer: UIView private let icon = ComponentView() private let title = ComponentView() private let subtitle = ComponentView() private let actionButton = ComponentView() private var skipButton: ComponentView? private var items: [Item] = [] private var component: PasskeysScreenIntroComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.scrollView = ScrollViewImpl() self.contentContainer = UIView() self.scrollView.addSubview(self.contentContainer) super.init(frame: frame) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } func update(component: PasskeysScreenIntroComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state self.backgroundColor = component.theme.list.plainBackgroundColor let sideInset: CGFloat = 16.0 let sideIconInset: CGFloat = 40.0 let itemsSideInset: CGFloat = sideInset + 20.0 var contentHeight: CGFloat = 0.0 //TODO:localize let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "passkey_logo"), loop: false )), environment: {}, containerSize: CGSize(width: 124.0, height: 124.0) ) let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight), size: iconSize) if let iconView = self.icon.view as? LottieComponent.View { if iconView.superview == nil { self.contentContainer.addSubview(iconView) iconView.playOnce() } transition.setPosition(view: iconView, position: iconFrame.center) iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) } contentHeight += iconSize.height contentHeight += 10.0 let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: "Protect your account", font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.contentContainer.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.center) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } contentHeight += titleSize.height contentHeight += 10.0 let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( text: .plain(NSAttributedString(string: "Log in safely and keep your account secure.", font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) if let subtitleView = self.subtitle.view { if subtitleView.superview == nil { self.contentContainer.addSubview(subtitleView) } transition.setPosition(view: subtitleView, position: subtitleFrame.center) subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) } contentHeight += subtitleSize.height contentHeight += 47.0 struct ItemDesc { var icon: String var title: String var text: String } let itemDescs: [ItemDesc] = [ ItemDesc( icon: "Settings/Passkeys/Intro1", title: "Create a Passkey", text: "Make a passkey to sign in easily and safely." ), ItemDesc( icon: "Settings/Passkeys/Intro2", title: "Log in with Face ID", text: "Use Face ID, Touch ID, or your passcode to sign in." ), ItemDesc( icon: "Settings/Passkeys/Intro3", title: "Store Passkey Securely", text: "Your passkey is safely kept in your iCloud Keychain." ) ] for i in 0 ..< itemDescs.count { if i != 0 { contentHeight += 24.0 } let item: Item if self.items.count > i { item = self.items[i] } else { item = Item() self.items.append(item) } let itemDesc = itemDescs[i] let iconSize = item.icon.update( transition: .immediate, component: AnyComponent(BundleIconComponent( name: itemDesc.icon, tintColor: component.theme.list.itemAccentColor )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let titleSize = item.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), maximumNumberOfLines: 0, lineSpacing: 0.2 )), environment: {}, containerSize: CGSize(width: availableSize.width - itemsSideInset * 2.0 - sideIconInset, height: 1000.0) ) let textSize = item.text.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), maximumNumberOfLines: 0, lineSpacing: 0.18 )), environment: {}, containerSize: CGSize(width: availableSize.width - itemsSideInset * 2.0 - sideIconInset, height: 1000.0) ) if let iconView = item.icon.view { if iconView.superview == nil { self.contentContainer.addSubview(iconView) } transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: itemsSideInset, y: contentHeight + 4.0), size: iconSize)) } if let titleView = item.title.view { if titleView.superview == nil { self.contentContainer.addSubview(titleView) } transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: itemsSideInset + sideIconInset, y: contentHeight), size: titleSize)) } contentHeight += titleSize.height contentHeight += 2.0 if let textView = item.text.view { if textView.superview == nil { self.contentContainer.addSubview(textView) } transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: itemsSideInset + sideIconInset, y: contentHeight), size: textSize)) } contentHeight += textSize.height } let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: component.insets.bottom, innerDiameter: 52.0, sideInset: 32.0) let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, color: component.theme.list.itemCheckColors.fillColor, foreground: component.theme.list.itemCheckColors.foregroundColor, pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), cornerRadius: 52.0 * 0.5 ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Create Passkey", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))) ), action: { [weak self] in guard let self, let component = self.component else { return } component.createPasskeyAction() } )), environment: {}, containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) if component.displaySkip { let skipButton: ComponentView } else if let skipButton = self.skipButton { self.skipButton = nil skipButton.view?.removeFromSuperview() } let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { self.addSubview(actionButtonView) } transition.setFrame(view: actionButtonView, frame: buttonFrame) } let contentTopInset = component.insets.top let contentBottomInset = availableSize.height - buttonFrame.minY let contentContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(contentTopInset, floor((availableSize.height - contentTopInset - contentBottomInset - contentHeight) * 0.5))), size: CGSize(width: availableSize.width, height: contentHeight)) transition.setFrame(view: self.contentContainer, frame: contentContainerFrame) self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) let scrollContentSize = CGSize(width: availableSize.width, height: contentContainerFrame.maxY) if self.scrollView.contentSize != scrollContentSize { self.scrollView.contentSize = scrollContentSize } let scrollInsets = UIEdgeInsets(top: component.insets.top, left: 0.0, bottom: 0.0, right: 0.0) if self.scrollView.verticalScrollIndicatorInsets != scrollInsets { self.scrollView.verticalScrollIndicatorInsets = scrollInsets } return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }