Files
Swiftgram/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift
2026-01-27 22:24:31 +04:00

613 lines
29 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import GlassBarButtonComponent
import ButtonComponent
import PresentationDataUtils
import BundleIconComponent
import ListSectionComponent
import ListActionItemComponent
import AvatarComponent
import Markdown
import PhoneNumberFormat
private final class AuthConfirmationSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: MessageActionUrlAuthResult
let completion: (Bool, Bool) -> Void
let cancel: (Bool) -> Void
init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void,
cancel: @escaping (Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
self.cancel = cancel
}
static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let subject: MessageActionUrlAuthResult
var peer: EnginePeer?
fileprivate var inProgress = false
var allowWrite = true
weak var controller: ViewController?
init(context: AccountContext, subject: MessageActionUrlAuthResult) {
self.context = context
self.subject = subject
super.init()
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.peer = peer
self.updated()
})
}
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
guard case let .request(domain, _, _, _) = self.subject else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, case let .user(user) = peer, let phone = user.phone else {
return
}
let phoneNumber = formatPhoneNumber(context: self.context, number: phone)
//TODO:localize
let alertController = textAlertController(
context: self.context,
title: "Phone Number",
text: "**\(domain)** wants to access your phone number **\(phoneNumber)**. Allow access?",
actions: [
TextAlertAction(type: .genericAction, title: "Deny", action: {
commit(false)
}),
TextAlertAction(type: .defaultAction, title: "Allow", action: {
commit(true)
})
]
)
self.controller?.present(alertController, in: .window(.root))
})
}
}
func makeState() -> State {
return State(context: self.context, subject: self.subject)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let peerButton = Child(AvatarComponent.self)
let avatar = Child(AvatarComponent.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let clientSection = Child(ListSectionComponent.self)
let optionsSection = Child(ListSectionComponent.self)
let cancelButton = Child(ButtonComponent.self)
let doneButton = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
if state.controller == nil {
state.controller = environment.controller()
}
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
let _ = strings
guard case let .request(domain, bot, clientData, flags) = component.subject else {
fatalError()
}
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
component.cancel(true)
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
if let peer = state.peer {
let peerButton = peerButton.update(
component: AvatarComponent(
context: component.context,
theme: environment.theme,
peer: peer
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(peerButton
.position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0))
)
}
var contentHeight: CGFloat = 32.0
let avatar = avatar.update(
component: AvatarComponent(
context: component.context,
theme: environment.theme,
peer: EnginePeer(bot),
clipStyle: .roundedRect
),
availableSize: CGSize(width: 92.0, height: 92.0),
transition: .immediate
)
context.add(avatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0))
)
contentHeight += avatar.size.height
contentHeight += 18.0
let titleFont = Font.bold(24.0)
let title = title.update(
component: MultilineTextComponent(
text: .markdown(text: "Log in to **\(domain)**", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
horizontalAlignment: .center,
maximumNumberOfLines: 2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0))
)
contentHeight += title.size.height
contentHeight += 16.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let description = description.update(
component: MultilineTextComponent(
text: .markdown(text: "This site will receive your **name**,\n**username** and **profile photo**.", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
horizontalAlignment: .center,
maximumNumberOfLines: 3,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0))
)
contentHeight += description.size.height
contentHeight += 16.0
var clientSectionItems: [AnyComponentWithIdentity<Empty>] = []
clientSectionItems.append(
AnyComponentWithIdentity(id: "device", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Device",
font: Font.regular(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "info",
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.platform ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.browser ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
textColor: theme.list.itemSecondaryTextColor
)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .right, spacing: 3.0)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
clientSectionItems.append(
AnyComponentWithIdentity(id: "region", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "IP Address",
font: Font.regular(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "info",
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.ip ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.region ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
textColor: theme.list.itemSecondaryTextColor
)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .right, spacing: 3.0)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
let clientSection = clientSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "This login attempt came from the device above.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: clientSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(clientSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0))
)
contentHeight += clientSection.size.height
if flags.contains(.requestWriteAccess) {
contentHeight += 38.0
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Messages",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in
guard let state else {
return
}
state.allowWrite = !state.allowWrite
state.updated()
})),
action: nil
))))
let optionsSection = optionsSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "This will allow \(EnginePeer(bot).compactDisplayTitle) to message you.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: optionsSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(optionsSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0))
)
contentHeight += optionsSection.size.height
}
contentHeight += 32.0
let buttonSpacing: CGFloat = 10.0
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonWidth = (context.availableSize.width - buttonInsets.left - buttonInsets.right - buttonSpacing) / 2.0
let cancelButton = cancelButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1),
foreground: theme.list.itemPrimaryTextColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Cancel", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center))))
),
action: {
component.cancel(true)
}
),
availableSize: CGSize(width: buttonWidth, height: 52.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0))
)
let doneButton = doneButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0,
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Log In", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
displaysProgress: state.inProgress,
action: { [weak state] in
guard let state else {
return
}
var allowWrite = false
if flags.contains(.requestWriteAccess) && state.allowWrite {
allowWrite = true
}
if flags.contains(.requestPhoneNumber) {
state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in
component.completion(allowWrite, sharePhoneNumber)
state.inProgress = true
state.updated()
})
} else {
component.completion(allowWrite, false)
state.inProgress = true
state.updated()
}
}
),
availableSize: CGSize(width: buttonWidth, height: 52.0),
transition: .immediate
)
context.add(doneButton
.position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0))
)
contentHeight += doneButton.size.height
contentHeight += buttonInsets.bottom
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
private final class AuthConfirmationSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: MessageActionUrlAuthResult
let completion: (Bool, Bool) -> Void
init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
}
static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(AuthConfirmationSheetContent(
context: context.component.context,
subject: context.component.subject,
completion: context.component.completion,
cancel: { animate in
if animate {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else if let controller = controller() {
controller.dismiss(animated: false, completion: nil)
}
}
)),
style: .glass,
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class AuthConfirmationScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let subject: MessageActionUrlAuthResult
fileprivate let completion: (Bool, Bool) -> Void
public init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
super.init(
context: context,
component: AuthConfirmationSheetComponent(
context: context,
subject: subject,
completion: completion
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}