Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PassportUI",
module_name = "PassportUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SSignalKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/PhoneInputNode:PhoneInputNode",
"//submodules/CountrySelectionUI:CountrySelectionUI",
"//submodules/GalleryUI:GalleryUI",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/LegacyUI:LegacyUI",
"//submodules/ImageCompression:ImageCompression",
"//submodules/DateSelectionUI:DateSelectionUI",
"//submodules/PasswordSetupUI:PasswordSetupUI",
"//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/Markdown:Markdown",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,11 @@
import Foundation
import TelegramCore
func findValue(_ values: [SecureIdValueWithContext], key: SecureIdValueKey) -> (Int, SecureIdValueWithContext)? {
for i in 0 ..< values.count {
if values[i].value.key == key {
return (i, values[i])
}
}
return nil
}
@@ -0,0 +1,122 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
protocol FormBlockItemNodeProto {
}
enum FormBlockItemInset {
case regular
case custom(CGFloat)
}
class FormBlockItemNode<Item: FormControllerItem>: ASDisplayNode, FormControllerItemNode, FormBlockItemNodeProto {
private let topSeparatorInset: FormBlockItemInset
private let highlightedBackgroundNode: ASDisplayNode
let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let bottomSeparatorNode: ASDisplayNode
private let selectionButtonNode: HighlightTrackingButtonNode
init(selectable: Bool, topSeparatorInset: FormBlockItemInset) {
self.topSeparatorInset = topSeparatorInset
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.alpha = 0.0
self.selectionButtonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.highlightedBackgroundNode)
if selectable {
self.selectionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
self.addSubnode(self.selectionButtonNode)
self.selectionButtonNode.addTarget(self, action: #selector(self.selectionButtonPressed), forControlEvents: .touchUpInside)
}
}
final func updateInternal(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
let (preLayout, apply) = self.update(item: item, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
return (preLayout, { params in
self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
let height = apply(params)
let topSeparatorInset: CGFloat
switch previousNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
switch self.topSeparatorInset {
case .regular:
topSeparatorInset = 16.0
case let .custom(value):
topSeparatorInset = value
}
default:
topSeparatorInset = 0.0
}
switch nextNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
self.bottomSeparatorNode.isHidden = true
default:
self.bottomSeparatorNode.isHidden = false
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.selectionButtonNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
return height
})
}
func update(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
preconditionFailure()
}
@objc private func selectionButtonPressed() {
self.selected()
}
func selected() {
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,49 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
public class FormController<InnerState, InitParams, Node: FormControllerNode<InitParams, InnerState>>: ViewController {
public var controllerNode: Node {
return self.displayNode as! Node
}
private let initParams: InitParams
private var presentationData: PresentationData
init(initParams: InitParams, presentationData: PresentationData) {
self.initParams = initParams
self.presentationData = presentationData
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.view.endEditing(true)
super.dismiss(completion: completion)
}
override public func loadDisplayNode() {
self.displayNode = Node(initParams: self.initParams, presentationData: self.presentationData)
self.controllerNode.present = { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
@@ -0,0 +1,81 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
enum FormControllerActionType {
case accent
case destructive
}
final class FormControllerActionItem: FormControllerItem {
let type: FormControllerActionType
let title: String
let fullTopInset: Bool
let activated: () -> Void
init(type: FormControllerActionType, title: String, fullTopInset: Bool = false, activated: @escaping () -> Void) {
self.type = type
self.title = title
self.fullTopInset = fullTopInset
self.activated = activated
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerActionItemNode(fullTopInset: self.fullTopInset)
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerActionItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerActionItemNode: FormBlockItemNode<FormControllerActionItem> {
private let titleNode: ImmediateTextNode
private var item: FormControllerActionItem?
init(fullTopInset: Bool = false) {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
super.init(selectable: true, topSeparatorInset: fullTopInset ? .custom(0.0) : .regular)
self.addSubnode(self.titleNode)
}
override func update(item: FormControllerActionItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
let titleColor: UIColor
switch item.type {
case .accent:
titleColor = theme.list.itemAccentColor
case .destructive:
titleColor = theme.list.itemDestructiveColor
}
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: titleColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
return (FormControllerItemPreLayout(aligningInset: 0.0), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize))
return 44.0
})
}
override func selected() {
self.item?.activated()
}
}
@@ -0,0 +1,115 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
private let errorFont = Font.regular(13.0)
final class FormControllerDetailActionItem: FormControllerItem {
let title: String
let text: String
let placeholder: String
let error: String?
let activated: () -> Void
init(title: String, text: String, placeholder: String, error: String?, activated: @escaping () -> Void) {
self.title = title
self.text = text
self.placeholder = placeholder
self.error = error
self.activated = activated
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerDetailActionItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerDetailActionItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerDetailActionItemNode: FormBlockItemNode<FormControllerDetailActionItem> {
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let errorNode: ImmediateTextNode
private var item: FormControllerDetailActionItem?
init() {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.errorNode = ImmediateTextNode()
self.errorNode.displaysAsynchronously = false
self.errorNode.maximumNumberOfLines = 0
super.init(selectable: true, topSeparatorInset: .regular)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.errorNode)
}
override func update(item: FormControllerDetailActionItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude))
let aligningInset: CGFloat
if titleSize.width.isZero {
aligningInset = 0.0
} else {
aligningInset = leftInset + titleSize.width + 17.0
}
return (FormControllerItemPreLayout(aligningInset: aligningInset), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleSize))
if !item.text.isEmpty {
self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: theme.list.itemPrimaryTextColor)
} else {
self.textNode.attributedText = NSAttributedString(string: item.placeholder, font: textFont, textColor: theme.list.itemPlaceholderTextColor)
}
let textSize = self.textNode.updateLayout(CGSize(width: width - params.maxAligningInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 11.0), size: textSize))
self.errorNode.attributedText = NSAttributedString(string: item.error ?? "", font: errorFont, textColor: theme.list.freeTextErrorColor)
let errorSize = self.errorNode.updateLayout(CGSize(width: width - params.maxAligningInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.errorNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 44.0 - 4.0), size: errorSize))
var height: CGFloat = 44.0
if !errorSize.width.isZero {
height += -4.0 + errorSize.height + 8.0
}
return height
})
}
func activate() {
self.item?.activated()
}
override func selected() {
activate()
}
}
@@ -0,0 +1,70 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let titleFont = Font.regular(14.0)
final class FormControllerHeaderItem: FormControllerItem {
let text: String
init(text: String) {
self.text = text
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerHeaderItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
guard let node = node as? FormControllerHeaderItemNode else {
assertionFailure()
return 0.0
}
return node.update(item: self, previousNeighbor: previousNeighbor, width: width, theme: theme, transition: transition)
})
}
}
final class FormControllerHeaderItemNode: ASDisplayNode, FormControllerItemNode {
private let textNode: ImmediateTextNode
override init() {
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
super.init()
self.addSubnode(self.textNode)
}
func update(item: FormControllerHeaderItem, previousNeighbor: FormControllerItemNeighbor, width: CGFloat, theme: PresentationTheme, transition: ContainedViewLayoutTransition) -> CGFloat {
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: theme.list.sectionHeaderTextColor)
let leftInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude))
let height: CGFloat
switch previousNeighbor {
case .none:
height = 20.0 + 30.0
case .spacer:
height = 14.0
case .item:
height = 14.0
}
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: height - 23.0), size: textSize))
return height
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,37 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import MergeLists
public protocol FormControllerEntry: Identifiable {
associatedtype ItemParams
func isEqual(to: Self) -> Bool
func item(params: ItemParams, strings: PresentationStrings) -> FormControllerItem
}
public enum FormControllerItemNeighbor {
case none
case spacer
case item(FormControllerItemNode)
}
public struct FormControllerItemPreLayout {
let aligningInset: CGFloat
}
public struct FormControllerItemLayoutParams {
let maxAligningInset: CGFloat
}
public protocol FormControllerItem {
func node() -> ASDisplayNode & FormControllerItemNode
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat)
}
public protocol FormControllerItemNode {
var preventsTouchesToOtherItems: Bool { get }
func touchesToOtherItemsPrevented()
}
@@ -0,0 +1,412 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
private func hasFirstResponder(_ view: UIView) -> Bool {
if view.isFirstResponder {
return true
}
for subview in view.subviews {
if hasFirstResponder(subview) {
return true
}
}
return false
}
struct FormControllerLayoutState {
var layout: ContainerViewLayout
var navigationHeight: CGFloat
func isEqual(to: FormControllerLayoutState) -> Bool {
if self.layout != to.layout {
return false
}
if self.navigationHeight != to.navigationHeight {
return false
}
return true
}
}
struct FormControllerPresentationState {
var theme: PresentationTheme
var strings: PresentationStrings
var dateTimeFormat: PresentationDateTimeFormat
func isEqual(to: FormControllerPresentationState) -> Bool {
if self.theme !== to.theme {
return false
}
if self.strings !== to.strings {
return false
}
if self.dateTimeFormat != to.dateTimeFormat {
return false
}
return true
}
}
struct FormControllerInternalState<InnerState: FormControllerInnerState> {
var layoutState: FormControllerLayoutState?
var presentationState: FormControllerPresentationState
var innerState: InnerState?
func isEqual(to: FormControllerInternalState) -> Bool {
if let lhsLayoutState = self.layoutState, let rhsLayoutState = to.layoutState {
if !lhsLayoutState.isEqual(to: rhsLayoutState) {
return false
}
} else if (self.layoutState != nil) != (to.layoutState != nil) {
return false
}
if !self.presentationState.isEqual(to: to.presentationState) {
return false
}
if let lhsInnerState = self.innerState, let rhsInnerState = to.innerState {
if !lhsInnerState.isEqual(to: rhsInnerState) {
return false
}
} else if (self.innerState != nil) != (to.innerState != nil) {
return false
}
return true
}
}
public struct FormControllerState<InnerState: FormControllerInnerState> {
let layoutState: FormControllerLayoutState
let presentationState: FormControllerPresentationState
let innerState: InnerState
}
public enum FormControllerItemEntry<Entry: FormControllerEntry> {
case entry(Entry)
case spacer
}
public protocol FormControllerInnerState {
associatedtype Entry: FormControllerEntry
func isEqual(to: Self) -> Bool
func entries() -> [FormControllerItemEntry<Entry>]
}
private enum FilteredItemNeighbor {
case spacer
case item(FormControllerItem)
}
public class FormControllerNode<InitParams, InnerState: FormControllerInnerState>: ViewControllerTracingNode, ASScrollViewDelegate {
private typealias InternalState = FormControllerInternalState<InnerState>
typealias State = FormControllerState<InnerState>
typealias Entry = InnerState.Entry
private var internalState: InternalState
var innerState: InnerState? {
return self.internalState.innerState
}
var layoutState: FormControllerLayoutState? {
return self.internalState.layoutState
}
let scrollNode: FormControllerScrollerNode
private var appliedLayout: FormControllerLayoutState?
private var appliedEntries: [Entry] = []
private(set) var itemNodes: [ASDisplayNode & FormControllerItemNode] = []
var present: (ViewController, Any?) -> Void = { _, _ in }
var itemParams: Entry.ItemParams {
preconditionFailure()
}
required public init(initParams: InitParams, presentationData: PresentationData) {
self.internalState = FormControllerInternalState(layoutState: nil, presentationState: FormControllerPresentationState(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat), innerState: nil)
self.scrollNode = FormControllerScrollerNode()
super.init()
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.scrollNode.backgroundColor = nil
self.scrollNode.isOpaque = false
self.scrollNode.delegate = self.wrappedScrollViewDelegate
self.addSubnode(self.scrollNode)
self.scrollNode.view.delaysContentTouches = true
self.scrollNode.touchesPrevented = { [weak self] position in
guard let strongSelf = self else {
return false
}
for i in 0 ..< strongSelf.itemNodes.count {
if strongSelf.itemNodes[i].preventsTouchesToOtherItems {
if let node = strongSelf.itemNodeAtPoint(position), node === strongSelf.itemNodes[i] {
return false
}
strongSelf.itemNodes[i].touchesToOtherItemsPrevented()
return true
}
}
return false
}
}
func itemNodeAtPoint(_ point: CGPoint) -> (ASDisplayNode & FormControllerItemNode)? {
for node in self.itemNodes {
if node.frame.contains(point) {
return node
}
}
return nil
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.updateInternalState(transition: transition, { state in
var state = state
state.layoutState = FormControllerLayoutState(layout: layout, navigationHeight: navigationHeight)
return state
})
}
func updateInnerState(transition: ContainedViewLayoutTransition, with innerState: InnerState) {
self.updateInternalState(transition: transition, { state in
var state = state
state.innerState = innerState
return state
})
}
private func updateInternalState(transition: ContainedViewLayoutTransition, _ f: (InternalState) -> InternalState) {
let updated = f(self.internalState)
if !updated.isEqual(to: self.internalState) {
self.internalState = updated
if let layoutState = updated.layoutState, let innerState = updated.innerState {
self.stateUpdated(state: FormControllerState(layoutState: layoutState, presentationState: updated.presentationState, innerState: innerState), transition: transition)
}
}
}
private weak var preivousItemNodeWithFocus: (ASDisplayNode & FormControllerItemNode)?
func stateUpdated(state: State, transition: ContainedViewLayoutTransition) {
let previousLayout = self.appliedLayout
self.appliedLayout = state.layoutState
let layout = state.layoutState.layout
var insets = layout.insets(options: [.input])
insets.top += max(state.layoutState.navigationHeight, layout.insets(options: [.statusBar]).top)
let entries = state.innerState.entries()
var filteredEntries: [Entry] = []
var filteredItemNeighbors: [FilteredItemNeighbor] = []
var itemNodes: [ASDisplayNode & FormControllerItemNode] = []
var insertedItemNodeIndices = Set<Int>()
for i in 0 ..< entries.count {
if case let .entry(entry) = entries[i] {
let item = entry.item(params: self.itemParams, strings: state.presentationState.strings)
filteredEntries.append(entry)
filteredItemNeighbors.append(.item(item))
var found = false
inner: for j in 0 ..< self.appliedEntries.count {
if entry.stableId == self.appliedEntries[j].stableId {
itemNodes.append(self.itemNodes[j])
found = true
break inner
}
}
if !found {
let itemNode = item.node()
insertedItemNodeIndices.insert(itemNodes.count)
itemNodes.append(itemNode)
self.scrollNode.addSubnode(itemNode)
}
} else {
filteredItemNeighbors.append(.spacer)
}
}
for itemNode in self.itemNodes {
var found = false
inner: for updated in itemNodes {
if updated === itemNode {
found = true
break inner
}
}
if !found {
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
}
}
self.appliedEntries = filteredEntries
self.itemNodes = itemNodes
var applyLayouts: [(ContainedViewLayoutTransition, FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat)] = []
var itemNodeIndex = 0
for i in 0 ..< filteredItemNeighbors.count {
if case let .item(item) = filteredItemNeighbors[i] {
let previousNeighbor: FormControllerItemNeighbor
let nextNeighbor: FormControllerItemNeighbor
if i != 0 {
switch filteredItemNeighbors[i - 1] {
case .spacer:
previousNeighbor = .spacer
case .item:
previousNeighbor = .item(itemNodes[itemNodeIndex - 1])
}
} else {
previousNeighbor = .none
}
if i != filteredItemNeighbors.count - 1 {
switch filteredItemNeighbors[i + 1] {
case .spacer:
nextNeighbor = .spacer
case .item:
nextNeighbor = .item(itemNodes[itemNodeIndex + 1])
}
} else {
nextNeighbor = .none
}
let itemTransition: ContainedViewLayoutTransition
if insertedItemNodeIndices.contains(i) {
itemTransition = .immediate
} else {
itemTransition = transition
}
let (preLayout, apply) = item.update(node: itemNodes[itemNodeIndex], theme: state.presentationState.theme, strings: state.presentationState.strings, dateTimeFormat: state.presentationState.dateTimeFormat, width: layout.size.width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: itemTransition)
applyLayouts.append((itemTransition, preLayout, apply))
itemNodeIndex += 1
}
}
var commonAligningInset: CGFloat = 0.0
for i in 0 ..< itemNodes.count {
commonAligningInset = max(commonAligningInset, applyLayouts[i].1.aligningInset)
}
var contentHeight: CGFloat = 0.0
itemNodeIndex = 0
for i in 0 ..< filteredItemNeighbors.count {
if case .item = filteredItemNeighbors[i] {
let itemNode = itemNodes[itemNodeIndex]
let (itemTransition, _, apply) = applyLayouts[itemNodeIndex]
let itemHeight = apply(FormControllerItemLayoutParams(maxAligningInset: commonAligningInset))
itemTransition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: itemHeight)))
contentHeight += itemHeight
itemNodeIndex += 1
} else {
contentHeight += 35.0
}
}
contentHeight += 36.0
let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight)
let previousBoundsOrigin = self.scrollNode.bounds.origin
self.scrollNode.view.ignoreUpdateBounds = true
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.scrollNode.view.contentSize = scrollContentSize
self.scrollNode.view.contentInset = insets
self.scrollNode.view.scrollIndicatorInsets = insets
self.scrollNode.view.ignoreUpdateBounds = false
var updatedItemNodeWithFocus: (ASDisplayNode & FormControllerItemNode)?
for itemNode in self.itemNodes {
if hasFirstResponder(itemNode.view) {
if self.preivousItemNodeWithFocus !== itemNode {
self.preivousItemNodeWithFocus = itemNode
updatedItemNodeWithFocus = itemNode
}
break
}
}
if let previousLayout = previousLayout {
var previousInsets = previousLayout.layout.insets(options: [.input])
previousInsets.top += max(previousLayout.navigationHeight, previousLayout.layout.insets(options: [.statusBar]).top)
let insetsScrollOffset = insets.top - previousInsets.top
let negativeOverscroll = min(previousBoundsOrigin.y + insets.top, 0.0)
let cleanOrigin = max(previousBoundsOrigin.y, -insets.top)
var contentOffset = CGPoint(x: 0.0, y: cleanOrigin + insetsScrollOffset)
contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height)
contentOffset.y = max(contentOffset.y, -insets.top)
contentOffset.y += negativeOverscroll
if let updatedItemNodeWithFocus = updatedItemNodeWithFocus {
let itemRect = updatedItemNodeWithFocus.view.convert(updatedItemNodeWithFocus.view.bounds, to: self.scrollNode.view)
if contentOffset.y + layout.size.height - insets.bottom < itemRect.maxY + 4.0 {
contentOffset.y = itemRect.maxY + 4.0 - (layout.size.height - insets.bottom)
}
}
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
} else {
let contentOffset = CGPoint(x: 0.0, y: -insets.top)
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size))
}
if previousLayout == nil {
self.didAppear()
}
}
func didAppear() {
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
func enumerateItemsAndEntries(_ f: (InnerState.Entry, ASDisplayNode & FormControllerItemNode) -> Bool) {
for i in 0 ..< self.appliedEntries.count {
if !f(self.appliedEntries[i], self.itemNodes[i]) {
break
}
}
}
func forceUpdateState(transition: ContainedViewLayoutTransition) {
if let layoutState = self.layoutState, let innerState = self.innerState {
self.stateUpdated(state: FormControllerState(layoutState: layoutState, presentationState: self.internalState.presentationState, innerState: innerState), transition: transition)
}
}
func scrollToItemNode(_ itemNode: ASDisplayNode & FormControllerItemNode) {
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: max(0.0, min(itemNode.frame.minY, self.scrollNode.view.contentSize.height - self.scrollNode.view.frame.height)))
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
final class FormControllerScrollerNodeView: UIScrollView {
weak var target: FormControllerScrollerNode?
var ignoreUpdateBounds = false
override init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
self.alwaysBounceVertical = true
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if !self.ignoreUpdateBounds {
super.bounds = value
}
}
}
override func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
}
override func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?, in view: UIView) -> Bool {
return self.target?.touchesShouldBegin(touches, with: event) ?? super.touchesShouldBegin(touches, with: event, in: view)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.target?.touchesShouldBegin(touches, with: event) ?? true {
super.touchesBegan(touches, with: event)
}
}
}
final class FormControllerScrollerNode: ASDisplayNode, ASScrollViewDelegate {
override var view: FormControllerScrollerNodeView {
return super.view as! FormControllerScrollerNodeView
}
weak var delegate: UIScrollViewDelegate?
var touchesPrevented: ((CGPoint) -> Bool)?
override init() {
super.init()
self.setViewBlock({
return FormControllerScrollerNodeView(frame: CGRect())
})
self.view.target = self
}
override func didLoad() {
super.didLoad()
self.view.delegate = self.wrappedScrollViewDelegate
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.delegate?.scrollViewDidScroll?(scrollView)
}
func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?) -> Bool {
let touchesPosition = touches.first!.location(in: self.view)
if let touchesPrevented = self.touchesPrevented {
if touchesPrevented(touchesPosition) {
return false
}
}
return true
}
}
@@ -0,0 +1,192 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let textFont = Font.regular(17.0)
private let errorFont = Font.regular(13.0)
enum FormControllerTextInputItemColor {
case primary
case error
}
enum FormControllerTextInputItemType: Equatable {
case regular(capitalization: UITextAutocapitalizationType, autocorrection: Bool)
case latin(capitalization: UITextAutocapitalizationType)
case email
case number
}
final class FormControllerTextInputItem: FormControllerItem {
let title: String
let text: String
let placeholder: String
let color: FormControllerTextInputItemColor
let type: FormControllerTextInputItemType
let returnKeyType: UIReturnKeyType
let error: String?
let textUpdated: (String) -> Void
let returnPressed: () -> Void
init(title: String, text: String, placeholder: String, color: FormControllerTextInputItemColor = .primary, type: FormControllerTextInputItemType, returnKeyType: UIReturnKeyType = .next, error: String? = nil, textUpdated: @escaping (String) -> Void, returnPressed: @escaping () -> Void) {
self.title = title
self.text = text
self.placeholder = placeholder
self.color = color
self.type = type
self.returnKeyType = returnKeyType
self.error = error
self.textUpdated = textUpdated
self.returnPressed = returnPressed
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerTextInputItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? FormControllerTextInputItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class FormControllerTextInputItemNode: FormBlockItemNode<FormControllerTextInputItem>, UITextFieldDelegate {
private let titleNode: ImmediateTextNode
private let errorNode: ImmediateTextNode
private let textField: TextFieldNode
private var item: FormControllerTextInputItem?
init() {
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.errorNode = ImmediateTextNode()
self.errorNode.displaysAsynchronously = false
self.errorNode.maximumNumberOfLines = 0
self.textField = TextFieldNode()
self.textField.textField.font = textFont
super.init(selectable: false, topSeparatorInset: .regular)
self.addSubnode(self.titleNode)
self.addSubnode(self.errorNode)
self.addSubnode(self.textField)
self.textField.textField.delegate = self
self.textField.textField.addTarget(self, action: #selector(self.editingChanged), for: [.editingChanged])
}
override func update(item: FormControllerTextInputItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
self.item = item
let leftInset: CGFloat = 16.0
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 70.0, height: CGFloat.greatestFiniteMagnitude))
let aligningInset: CGFloat
if titleSize.width.isZero {
aligningInset = 0.0
} else {
aligningInset = leftInset + titleSize.width + 17.0
}
return (FormControllerItemPreLayout(aligningInset: aligningInset), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleSize))
let capitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let keyboardType: UIKeyboardType
switch item.type {
case let .regular(capitalization, autocorrection):
capitalizationType = capitalization
autocorrectionType = autocorrection ? .default : .no
keyboardType = .default
case let .latin(capitalization):
capitalizationType = capitalization
autocorrectionType = .no
keyboardType = .asciiCapable
case .email:
capitalizationType = .none
autocorrectionType = .no
keyboardType = .emailAddress
case .number:
capitalizationType = .none
autocorrectionType = .no
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
keyboardType = .asciiCapableNumberPad
} else {
keyboardType = .numberPad
}
}
if self.textField.textField.keyboardType != keyboardType {
self.textField.textField.keyboardType = keyboardType
}
if self.textField.textField.autocapitalizationType != capitalizationType {
self.textField.textField.autocapitalizationType = capitalizationType
}
if self.textField.textField.autocorrectionType != autocorrectionType {
self.textField.textField.autocorrectionType = autocorrectionType
}
if self.textField.textField.returnKeyType != item.returnKeyType {
self.textField.textField.returnKeyType = item.returnKeyType
}
self.textField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textField.textField.tintColor = theme.list.itemAccentColor
let attributedPlaceholder = NSAttributedString(string: item.placeholder, font: textFont, textColor: theme.list.itemPlaceholderTextColor)
if !(self.textField.textField.attributedPlaceholder?.isEqual(to: attributedPlaceholder) ?? false) {
self.textField.textField.attributedPlaceholder = attributedPlaceholder
}
switch item.color {
case .primary:
self.textField.textField.textColor = theme.list.itemPrimaryTextColor
case .error:
self.textField.textField.textColor = theme.list.itemDestructiveColor
}
if self.textField.textField.text != item.text {
self.textField.textField.text = item.text
}
transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 3.0), size: CGSize(width: max(1.0, width - params.maxAligningInset - 8.0), height: 40.0)))
self.errorNode.attributedText = NSAttributedString(string: item.error ?? "", font: errorFont, textColor: theme.list.freeTextErrorColor)
let errorSize = self.errorNode.updateLayout(CGSize(width: width - params.maxAligningInset - 8.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.errorNode, frame: CGRect(origin: CGPoint(x: params.maxAligningInset, y: 44.0 - 4.0), size: errorSize))
var height: CGFloat = 44.0
if !errorSize.width.isZero {
height += -4.0 + errorSize.height + 8.0
}
return height
})
}
@objc private func editingChanged() {
self.item?.textUpdated(self.textField.textField.text ?? "")
}
@objc func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.item?.returnPressed()
return false
}
func activate() {
self.textField.textField.becomeFirstResponder()
}
}
@@ -0,0 +1,74 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
private let titleFont = Font.regular(14.0)
enum FormControllerTextItemColor {
case standard
case error
}
final class FormControllerTextItem: FormControllerItem {
let text: String
let color: FormControllerTextItemColor
init(text: String, color: FormControllerTextItemColor = .standard) {
self.text = text
self.color = color
}
func node() -> ASDisplayNode & FormControllerItemNode {
return FormControllerTextItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
guard let node = node as? FormControllerTextItemNode else {
assertionFailure()
return 0.0
}
return node.update(item: self, width: width, theme: theme, transition: transition)
})
}
}
final class FormControllerTextItemNode: ASDisplayNode, FormControllerItemNode {
private let textNode: ImmediateTextNode
override init() {
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
super.init()
self.addSubnode(self.textNode)
}
func update(item: FormControllerTextItem, width: CGFloat, theme: PresentationTheme, transition: ContainedViewLayoutTransition) -> CGFloat {
let color: UIColor
switch item.color {
case .standard:
color = theme.list.freeTextColor
case .error:
color = theme.list.freeTextErrorColor
}
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: color)
let leftInset: CGFloat = 16.0
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - 10.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: textSize))
return textSize.height + 14.0
}
var preventsTouchesToOtherItems: Bool {
return false
}
func touchesToOtherItemsPrevented() {
}
}
@@ -0,0 +1,476 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
class FormEditableBlockItemNode<Item: FormControllerItem>: ASDisplayNode, FormControllerItemNode, FormBlockItemNodeProto, ASGestureRecognizerDelegate {
private let topSeparatorInset: FormBlockItemInset
private let highlightedBackgroundNode: ASDisplayNode
let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let bottomSeparatorNode: ASDisplayNode
private let selectionButtonNode: HighlightTrackingButtonNode
private var leftRevealNode: ItemListRevealOptionsNode?
private var rightRevealNode: ItemListRevealOptionsNode?
private var revealOptions: (left: [ItemListRevealOption], right: [ItemListRevealOption]) = ([], [])
private var initialRevealOffset: CGFloat = 0.0
public private(set) var revealOffset: CGFloat = 0.0
private var recognizer: ItemListRevealOptionsGestureRecognizer?
private var hapticFeedback: HapticFeedback?
private var allowAnyDirection = false
private var validLayout: (CGSize, CGFloat, CGFloat)?
var isDisplayingRevealedOptions: Bool {
return !self.revealOffset.isZero
}
var canBeSelected: Bool {
return !self.isDisplayingRevealedOptions
}
init(selectable: Bool, topSeparatorInset: FormBlockItemInset) {
self.topSeparatorInset = topSeparatorInset
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.alpha = 0.0
self.selectionButtonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.highlightedBackgroundNode)
if selectable {
self.selectionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, strongSelf.canBeSelected {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
}
}
self.addSubnode(self.selectionButtonNode)
self.selectionButtonNode.addTarget(self, action: #selector(self.selectionButtonPressed), forControlEvents: .touchUpInside)
}
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer
recognizer.allowAnyDirection = self.allowAnyDirection
self.view.addGestureRecognizer(recognizer)
}
func setRevealOptions(_ options: (left: [ItemListRevealOption], right: [ItemListRevealOption])) {
if self.revealOptions == options {
return
}
let previousOptions = self.revealOptions
let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty
self.revealOptions = options
let isEmpty = options.left.isEmpty && options.right.isEmpty
if options.left.isEmpty {
if let _ = self.leftRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.left != options.left {
/*if let _ = self.leftRevealNode {
self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}*/
}
if options.right.isEmpty {
if let _ = self.rightRevealNode {
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else if previousOptions.right != options.right {
if let _ = self.rightRevealNode {
/*self.revealOptionsInteractivelyClosed()
self.recognizer?.becomeCancelled()
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))*/
}
}
if wasEmpty != isEmpty {
self.recognizer?.isEnabled = !isEmpty
}
let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
if self.isNodeLoaded {
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = self.recognizer, otherGestureRecognizer == recognizer {
return true
} else {
return false
}
}
@objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
guard let (size, _, _) = self.validLayout else {
return
}
switch recognizer.state {
case .began:
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x < revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.bounds.size
let location = recognizer.location(in: self.view)
if location.x > size.width - revealSize.width {
recognizer.becomeCancelled()
} else {
self.initialRevealOffset = self.revealOffset
}
} else {
if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty {
recognizer.becomeCancelled()
}
self.initialRevealOffset = self.revealOffset
}
case .changed:
var translation = recognizer.translation(in: self.view)
translation.x += self.initialRevealOffset
if self.revealOptions.left.isEmpty {
translation.x = min(0.0, translation.x)
}
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
self.setupAndAddLeftRevealNode()
self.revealOptionsInteractivelyOpened()
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
self.setupAndAddRightRevealNode()
self.revealOptionsInteractivelyOpened()
}
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
if self.leftRevealNode == nil && self.rightRevealNode == nil {
self.revealOptionsInteractivelyClosed()
}
case .ended, .cancelled:
guard let recognizer = self.recognizer else {
break
}
if let leftRevealNode = self.leftRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = leftRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset > 0.0 {
reveal = true
} else if self.revealOffset > revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x > 0.0 {
reveal = true
} else {
reveal = false
}
}
var selectedOption: ItemListRevealOption?
if reveal && leftRevealNode.isDisplayingExtendedAction() {
reveal = false
selectedOption = self.revealOptions.left.first
} else {
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
}
if let selectedOption = selectedOption {
self.revealOptionSelected(selectedOption, animated: true)
} else {
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
} else if let rightRevealNode = self.rightRevealNode {
let velocity = recognizer.velocity(in: self.view)
let revealSize = rightRevealNode.bounds.size
var reveal = false
if abs(velocity.x) < 100.0 {
if self.initialRevealOffset.isZero && self.revealOffset < 0.0 {
reveal = true
} else if self.revealOffset < -revealSize.width {
reveal = true
} else {
reveal = false
}
} else {
if velocity.x < 0.0 {
reveal = true
} else {
reveal = false
}
}
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
if !reveal {
self.revealOptionsInteractivelyClosed()
}
}
default:
break
}
}
private func setupAndAddLeftRevealNode() {
if !self.revealOptions.left.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.left, isLeft: true, enableAnimations: true)
self.leftRevealNode = revealNode
if let (size, leftInset, _) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += leftInset
revealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
}
self.addSubnode(revealNode)
}
}
private func setupAndAddRightRevealNode() {
if !self.revealOptions.right.isEmpty {
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
self?.revealOptionSelected(option, animated: false)
}, tapticAction: { [weak self] in
self?.hapticImpact()
})
revealNode.setOptions(self.revealOptions.right, isLeft: false, enableAnimations: true)
self.rightRevealNode = revealNode
if let (size, _, rightInset) = self.validLayout {
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
revealSize.width += rightInset
revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
}
self.addSubnode(revealNode)
}
}
final func updateInternal(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
let (preLayout, apply) = self.update(item: item, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
return (preLayout, { params in
self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
let height = apply(params)
let topSeparatorInset: CGFloat
switch previousNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
switch self.topSeparatorInset {
case .regular:
topSeparatorInset = 16.0
case let .custom(value):
topSeparatorInset = value
}
default:
topSeparatorInset = 0.0
}
switch nextNeighbor {
case let .item(item) where item is FormBlockItemNodeProto:
self.bottomSeparatorNode.isHidden = true
default:
self.bottomSeparatorNode.isHidden = false
}
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
//revealSize.width += leftInset
leftRevealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
}
if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
//revealSize.width += rightInset
rightRevealNode.frame = CGRect(origin: CGPoint(x: width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
}
self.validLayout = (CGSize(width: width, height: height), 0.0, 0.0)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.selectionButtonNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
return height
})
}
func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.revealOffset = offset
guard let (size, leftInset, rightInset) = self.validLayout else {
return
}
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
//let revealNodeOffset = max(-self.revealOffset, revealSize.width)
let revealNodeOffset = -self.revealOffset
leftRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition)
if CGFloat(offset).isLessThanOrEqualTo(0.0) {
self.leftRevealNode = nil
transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { [weak leftRevealNode] _ in
leftRevealNode?.removeFromSupernode()
})
} else {
transition.updateFrame(node: leftRevealNode, frame: revealFrame)
}
}
if let rightRevealNode = self.rightRevealNode {
let revealSize = rightRevealNode.bounds.size
let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
let revealNodeOffset = -max(self.revealOffset, -revealSize.width)
rightRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition)
if CGFloat(0.0).isLessThanOrEqualTo(offset) {
self.rightRevealNode = nil
transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { [weak rightRevealNode] _ in
rightRevealNode?.removeFromSupernode()
})
} else {
transition.updateFrame(node: rightRevealNode, frame: revealFrame)
}
}
let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero
if allowAnyDirection != self.allowAnyDirection {
self.allowAnyDirection = allowAnyDirection
self.recognizer?.allowAnyDirection = allowAnyDirection
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
}
self.updateRevealOffset(offset: offset, transition: transition)
}
func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
}
func revealOptionsInteractivelyOpened() {
}
func revealOptionsInteractivelyClosed() {
}
func setRevealOptionsOpened(_ value: Bool, animated: Bool) {
if value != !self.revealOffset.isZero {
if !self.revealOffset.isZero {
self.recognizer?.becomeCancelled()
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
} else {
transition = .immediate
}
if value {
if self.rightRevealNode == nil {
self.setupAndAddRightRevealNode()
if let rightRevealNode = self.rightRevealNode, rightRevealNode.isNodeLoaded, let _ = self.validLayout {
rightRevealNode.layout()
let revealSize = rightRevealNode.bounds.size
self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition)
}
}
} else if !self.revealOffset.isZero {
self.updateRevealOffsetInternal(offset: 0.0, transition: transition)
}
}
}
func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
}
private func hapticImpact() {
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.impact(.medium)
}
func update(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
preconditionFailure()
}
@objc private func selectionButtonPressed() {
if self.canBeSelected {
self.selected()
} else {
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.revealOptionsInteractivelyClosed()
}
}
func selected() {
}
var preventsTouchesToOtherItems: Bool {
return self.isDisplayingRevealedOptions
}
func touchesToOtherItemsPrevented() {
if self.isDisplayingRevealedOptions {
self.setRevealOptionsOpened(false, animated: true)
}
}
}
@@ -0,0 +1,526 @@
import Foundation
import UIKit
import LegacyComponents
import Display
import SSignalKit
import SwiftSignalKit
import Postbox
import TelegramCore
import OverlayStatusController
import AccountContext
import LegacyUI
import ImageCompression
import PresentationDataUtils
enum SecureIdAttachmentMenuType {
case generic
case idCard
case multiple
case selfie
}
struct SecureIdRecognizedDocumentData {
let documentType: String?
let documentSubtype: String?
let issuingCountry: String?
let nationality: String?
let lastName: String?
let firstName: String?
let documentNumber: String?
let birthDate: Date?
let gender: String?
let expiryDate: Date?
}
func presentLegacySecureIdAttachmentMenu(context: AccountContext, present: @escaping (ViewController) -> Void, validLayout: ContainerViewLayout, type: SecureIdAttachmentMenuType, recognizeDocumentData: Bool, completion: @escaping ([TelegramMediaResource], SecureIdRecognizedDocumentData?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: validLayout)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
present(legacyController)
let mappedIntent: TGPassportAttachIntent
switch type {
case .generic:
mappedIntent = TGPassportAttachIntentDefault
case .idCard:
mappedIntent = TGPassportAttachIntentIdentityCard
case .multiple:
mappedIntent = TGPassportAttachIntentMultiple
case .selfie:
mappedIntent = TGPassportAttachIntentSelfie
}
var uploadStarted = false
guard let attachMenu = TGPassportAttachMenu.present(with: legacyController.context, parentController: emptyController, menuController: nil, title: "", intent: mappedIntent, uploadAction: { signal, completed in
if let signal = signal {
if uploadStarted {
completed?()
return
}
uploadStarted = true
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
let statusDisposable = (Signal<Never, NoError> { subscriber in
present(statusController)
return ActionDisposable {
statusController.dismiss()
}
}
|> runOn(.mainQueue())
|> delay(0.1, queue: .mainQueue())).start()
let _ = (processedLegacySecureIdAttachmentItems(postbox: context.account.postbox, signal: signal)
|> mapToSignal { resources -> Signal<([TelegramMediaResource], SecureIdRecognizedDocumentData?), NoError> in
switch type {
case .generic, .idCard:
if recognizeDocumentData {
return recognizedResources(postbox: context.account.postbox, resources: resources, shouldBeDriversLicense: false)
|> map { data -> ([TelegramMediaResource], SecureIdRecognizedDocumentData?) in
return (resources, data)
}
}
default:
break
}
return .single((resources, nil))
}
|> deliverOnMainQueue).start(next: { resourcesAndData in
completion(resourcesAndData.0, resourcesAndData.1)
statusDisposable.dispose()
completed?()
})
} else {
completed?()
}
}, sourceView: nil, sourceRect: nil, barButtonItem: nil) else {
legacyController.dismiss()
return
}
attachMenu.didDismiss = { [weak legacyController] _ in
legacyController?.dismiss()
}
attachMenu.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
private enum AttachmentItem {
case image(UIImage)
case iCloud(URL)
}
private func processedLegacySecureIdAttachmentItems(postbox: Postbox, signal: SSignal) -> Signal<[TelegramMediaResource], NoError> {
let nativeSignal = Signal<AttachmentItem?, NoError> { subscriber in
let disposable = signal.start(next: { next in
if let dict = next as? [String: Any], let image = dict["image"] as? UIImage {
subscriber.putNext(.image(image))
} else if let next = next as? URL {
subscriber.putNext(.iCloud(next))
}
}, error: nil, completed: {
subscriber.putCompletion()
})
return ActionDisposable {
disposable?.dispose()
}
}
let collectedSignal = nativeSignal
|> mapToSignal { value -> Signal<UIImage?, NoError> in
guard let value = value else {
return .single(nil)
}
switch value {
case let .image(image):
return .single(image)
case let .iCloud(url):
return Signal<UIImage?, NoError> { subscriber in
let disposable = TGPassportICloud.fetchFile(with: url).start(next: { next in
if let url = next as? URL {
subscriber.putNext(UIImage(contentsOfFile: url.path))
}
}, completed: {
subscriber.putCompletion()
})
return ActionDisposable {
disposable?.dispose()
}
}
}
}
|> map { image -> [TelegramMediaResource] in
guard let image = image else {
return []
}
let randomId = Int64.random(in: Int64.min ... Int64.max)
let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpeg"
let scaledSize = image.size.aspectFitted(CGSize(width: 2048.0, height: 2048.0))
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
if let scaledImage = TGScaleImageToPixelSize(image, scaledSize), let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.84, tempFilePath: tempFile.path) {
let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath))
let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId)
return [resource]
} else {
return []
}
}
let collectedItems: Signal<[TelegramMediaResource], NoError> = collectedSignal
|> reduceLeft(value: [] as [TelegramMediaResource], f: { (list: [TelegramMediaResource], rest: [TelegramMediaResource]) -> [TelegramMediaResource] in
var list = list
list.append(contentsOf: rest)
return list
})
return collectedItems
}
private func recognizedResources(postbox: Postbox, resources: [TelegramMediaResource], shouldBeDriversLicense: Bool) -> Signal<SecureIdRecognizedDocumentData?, NoError> {
var signals: [Signal<SecureIdRecognizedDocumentData?, NoError>] = []
for resource in resources {
let image = Signal<UIImage?, NoError> { subscriber in
let fetch = postbox.mediaBox.fetchedResource(resource, parameters: nil).start()
let data = (postbox.mediaBox.resourceData(resource)
|> map { data -> UIImage? in
if data.complete {
return UIImage(contentsOfFile: data.path)
}
return nil
}).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
})
return ActionDisposable {
fetch.dispose()
data.dispose()
}
}
|> last
let recognized = image
|> mapToSignal { image -> Signal<SecureIdRecognizedDocumentData?, NoError> in
if let image = image {
return Signal { subscriber in
let disposable = TGPassportOCR.recognizeData(in: image, shouldBeDriversLicense: shouldBeDriversLicense)?.start(next: { value in
if let value = value as? TGPassportMRZ {
var issuingCountry: String? = nil
if let issuingCountryValue = value.issuingCountry {
issuingCountry = countryCodeAlpha3ToAlpha2(issuingCountryValue)
}
var nationality: String? = nil
if let nationalityValue = value.nationality {
nationality = countryCodeAlpha3ToAlpha2(nationalityValue)
}
subscriber.putNext(SecureIdRecognizedDocumentData(documentType: value.documentType, documentSubtype: value.documentSubtype, issuingCountry: issuingCountry, nationality: nationality, lastName: value.lastName.capitalized, firstName: value.firstName.capitalized, documentNumber: value.documentNumber, birthDate: value.birthDate, gender: value.gender, expiryDate: value.expiryDate))
subscriber.putCompletion()
} else {
subscriber.putNext(nil)
subscriber.putCompletion()
}
}, completed: nil)
return ActionDisposable {
disposable?.dispose()
}
}
} else {
return .single(nil)
}
}
signals.append(recognized)
}
return combineLatest(signals)
|> map { values -> SecureIdRecognizedDocumentData? in
for value in values {
if let value = value {
return value
}
}
return nil
}
}
private struct IsoCountryInfo {
var name: String
var numeric: String
var alpha2: String
var alpha3: String
var calling: String
var currency: String
var continent: String
}
func countryCodeAlpha3ToAlpha2(_ code: String) -> String? {
for country in IsoCountries.allCountries {
if country.alpha3 == code {
return country.alpha2
}
}
return nil
}
private class IsoCountries {
class var allCountries: Array<IsoCountryInfo> {
get {
return [
IsoCountryInfo(name: "Afghanistan", numeric: "004", alpha2: "AF", alpha3: "AFG", calling: "+93", currency: "AFN", continent: "AS" ),
IsoCountryInfo(name: "Åland Islands", numeric: "248", alpha2: "AX", alpha3: "ALA", calling: "+358", currency: "FIM", continent: "EU" ),
IsoCountryInfo(name: "Albania", numeric: "008", alpha2: "AL", alpha3: "ALB", calling: "+355", currency: "ALL", continent: "EU" ),
IsoCountryInfo(name: "Algeria", numeric: "012", alpha2: "DZ", alpha3: "DZA", calling: "+213", currency: "DZD", continent: "AF" ),
IsoCountryInfo(name: "American Samoa", numeric: "016", alpha2: "AS", alpha3: "ASM", calling: "+684", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Andorra", numeric: "020", alpha2: "AD", alpha3: "AND", calling: "+376", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Angola", numeric: "024", alpha2: "AO", alpha3: "AGO", calling: "+244", currency: "AOA", continent: "AF" ),
IsoCountryInfo(name: "Anguilla", numeric: "660", alpha2: "AI", alpha3: "AIA", calling: "+264", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Antarctica", numeric: "010", alpha2: "AQ", alpha3: "ATA", calling: "+672", currency: "AUD", continent: "AN" ),
IsoCountryInfo(name: "Antigua and Barbuda", numeric: "028", alpha2: "AG", alpha3: "ATG", calling: "+268", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Argentina", numeric: "032", alpha2: "AR", alpha3: "ARG", calling: "+54", currency: "ARS", continent: "SA" ),
IsoCountryInfo(name: "Armenia", numeric: "051", alpha2: "AM", alpha3: "ARM", calling: "+374", currency: "AMD", continent: "AS" ),
IsoCountryInfo(name: "Aruba", numeric: "533", alpha2: "AW", alpha3: "ABW", calling: "+297", currency: "AWG", continent: "NA" ),
IsoCountryInfo(name: "Australia", numeric: "036", alpha2: "AU", alpha3: "AUS", calling: "+61", currency: "AUD", continent: "OC" ),
IsoCountryInfo(name: "Austria", numeric: "040", alpha2: "AT", alpha3: "AUT", calling: "+43", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Azerbaijan", numeric: "031", alpha2: "AZ", alpha3: "AZE", calling: "+994", currency: "AZN", continent: "AS" ),
IsoCountryInfo(name: "Bahamas", numeric: "044", alpha2: "BS", alpha3: "BHS", calling: "+242", currency: "BSD", continent: "NA" ),
IsoCountryInfo(name: "Bahrain", numeric: "048", alpha2: "BH", alpha3: "BHR", calling: "+973", currency: "BHD", continent: "AS" ),
IsoCountryInfo(name: "Bangladesh", numeric: "050", alpha2: "BD", alpha3: "BGD", calling: "+880", currency: "BDT", continent: "AS" ),
IsoCountryInfo(name: "Barbados", numeric: "052", alpha2: "BB", alpha3: "BRB", calling: "+246", currency: "BBD", continent: "NA" ),
IsoCountryInfo(name: "Belarus", numeric: "112", alpha2: "BY", alpha3: "BLR", calling: "+375", currency: "BYR", continent: "EU" ),
IsoCountryInfo(name: "Belgium", numeric: "056", alpha2: "BE", alpha3: "BEL", calling: "+32", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Belize", numeric: "084", alpha2: "BZ", alpha3: "BLZ", calling: "+501", currency: "BZD", continent: "NA" ),
IsoCountryInfo(name: "Benin", numeric: "204", alpha2: "BJ", alpha3: "BEN", calling: "+229", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Bermuda", numeric: "060", alpha2: "BM", alpha3: "BMU", calling: "+441", currency: "BMD", continent: "NA" ),
IsoCountryInfo(name: "Bhutan", numeric: "064", alpha2: "BT", alpha3: "BTN", calling: "+975", currency: "BTN", continent: "AS" ),
IsoCountryInfo(name: "Bolivia, Plurinational State of", numeric: "068", alpha2: "BO", alpha3: "BOL", calling: "+591", currency: "BOB", continent: "SA" ),
IsoCountryInfo(name: "Bonaire, Sint Eustatius and Saba", numeric: "535", alpha2: "BQ", alpha3: "BES", calling: "+599", currency: "USD", continent: "" ),
IsoCountryInfo(name: "Bosnia and Herzegovina", numeric: "070", alpha2: "BA", alpha3: "BIH", calling: "+387", currency: "BAM", continent: "EU" ),
IsoCountryInfo(name: "Botswana", numeric: "072", alpha2: "BW", alpha3: "BWA", calling: "+267", currency: "BWP", continent: "AF" ),
IsoCountryInfo(name: "Bouvet Island", numeric: "074", alpha2: "BV", alpha3: "BVT", calling: "+47", currency: "NOK", continent: "AN" ),
IsoCountryInfo(name: "Brazil", numeric: "076", alpha2: "BR", alpha3: "BRA", calling: "+55", currency: "BRL", continent: "SA" ),
IsoCountryInfo(name: "British Indian Ocean Territory", numeric: "086", alpha2: "IO", alpha3: "IOT", calling: "+246", currency: "USD", continent: "AS" ),
IsoCountryInfo(name: "Brunei Darussalam", numeric: "096", alpha2: "BN", alpha3: "BRN", calling: "+673", currency: "BND", continent: "AS" ),
IsoCountryInfo(name: "Bulgaria", numeric: "100", alpha2: "BG", alpha3: "BGR", calling: "+359", currency: "BGN", continent: "EU" ),
IsoCountryInfo(name: "Burkina Faso", numeric: "854", alpha2: "BF", alpha3: "BFA", calling: "+226", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Burundi", numeric: "108", alpha2: "BI", alpha3: "BDI", calling: "+257", currency: "BIF", continent: "AF" ),
IsoCountryInfo(name: "Cambodia", numeric: "116", alpha2: "KH", alpha3: "KHM", calling: "+855", currency: "KHR", continent: "AS" ),
IsoCountryInfo(name: "Cameroon", numeric: "120", alpha2: "CM", alpha3: "CMR", calling: "+237", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Canada", numeric: "124", alpha2: "CA", alpha3: "CAN", calling: "+1", currency: "CAD", continent: "NA" ),
IsoCountryInfo(name: "Cabo Verde", numeric: "132", alpha2: "CV", alpha3: "CPV", calling: "+238", currency: "CVE", continent: "AF" ),
IsoCountryInfo(name: "Cayman Islands", numeric: "136", alpha2: "KY", alpha3: "CYM", calling: "+345", currency: "KYD", continent: "NA" ),
IsoCountryInfo(name: "Central African Republic", numeric: "140", alpha2: "CF", alpha3: "CAF", calling: "+236", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Chad", numeric: "148", alpha2: "TD", alpha3: "TCD", calling: "+235", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Chile", numeric: "152", alpha2: "CL", alpha3: "CHL", calling: "+56", currency: "CLP", continent: "SA" ),
IsoCountryInfo(name: "China", numeric: "156", alpha2: "CN", alpha3: "CHN", calling: "+86", currency: "CNY", continent: "AS" ),
IsoCountryInfo(name: "Christmas Island", numeric: "162", alpha2: "CX", alpha3: "CXR", calling: "+61", currency: "AUD", continent: "AS" ),
IsoCountryInfo(name: "Cocos (Keeling) Islands", numeric: "166", alpha2: "CC", alpha3: "CCK", calling: "+891", currency: "AUD", continent: "AS" ),
IsoCountryInfo(name: "Colombia", numeric: "170", alpha2: "CO", alpha3: "COL", calling: "+57", currency: "COP", continent: "SA" ),
IsoCountryInfo(name: "Comoros", numeric: "174", alpha2: "KM", alpha3: "COM", calling: "+269", currency: "KMF", continent: "AF" ),
IsoCountryInfo(name: "Congo", numeric: "178", alpha2: "CG", alpha3: "COG", calling: "+242", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Congo, the Democratic Republic of the", numeric: "180", alpha2: "CD", alpha3: "COD", calling: "+243", currency: "CDF", continent: "AF" ),
IsoCountryInfo(name: "Cook Islands", numeric: "184", alpha2: "CK", alpha3: "COK", calling: "+682", currency: "NZD", continent: "OC" ),
IsoCountryInfo(name: "Costa Rica", numeric: "188", alpha2: "CR", alpha3: "CRI", calling: "+506", currency: "CRC", continent: "NA" ),
IsoCountryInfo(name: "Côte d'Ivoire", numeric: "384", alpha2: "CI", alpha3: "CIV", calling: "+225", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Croatia", numeric: "191", alpha2: "HR", alpha3: "HRV", calling: "+385", currency: "HRK", continent: "EU" ),
IsoCountryInfo(name: "Cuba", numeric: "192", alpha2: "CU", alpha3: "CUB", calling: "+53", currency: "CUP", continent: "NA" ),
IsoCountryInfo(name: "Curaçao", numeric: "531", alpha2: "CW", alpha3: "CUW", calling: "+599", currency: "ANG", continent: "" ),
IsoCountryInfo(name: "Cyprus", numeric: "196", alpha2: "CY", alpha3: "CYP", calling: "+357", currency: "EUR", continent: "AS" ),
IsoCountryInfo(name: "Czech Republic", numeric: "203", alpha2: "CZ", alpha3: "CZE", calling: "+420", currency: "CZK", continent: "EU" ),
IsoCountryInfo(name: "Denmark", numeric: "208", alpha2: "DK", alpha3: "DNK", calling: "+45", currency: "DKK", continent: "EU" ),
IsoCountryInfo(name: "Djibouti", numeric: "262", alpha2: "DJ", alpha3: "DJI", calling: "+253", currency: "DJF", continent: "AF" ),
IsoCountryInfo(name: "Dominica", numeric: "212", alpha2: "DM", alpha3: "DMA", calling: "+767", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Dominican Republic", numeric: "214", alpha2: "DO", alpha3: "DOM", calling: "+809", currency: "DOP", continent: "NA" ),
IsoCountryInfo(name: "Ecuador", numeric: "218", alpha2: "EC", alpha3: "ECU", calling: "+593", currency: "USD", continent: "SA" ),
IsoCountryInfo(name: "Egypt", numeric: "818", alpha2: "EG", alpha3: "EGY", calling: "+20", currency: "EGP", continent: "AF" ),
IsoCountryInfo(name: "El Salvador", numeric: "222", alpha2: "SV", alpha3: "SLV", calling: "+503", currency: "SVC", continent: "NA" ),
IsoCountryInfo(name: "Equatorial Guinea", numeric: "226", alpha2: "GQ", alpha3: "GNQ", calling: "+240", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Eritrea", numeric: "232", alpha2: "ER", alpha3: "ERI", calling: "+291", currency: "ETB", continent: "AF" ),
IsoCountryInfo(name: "Estonia", numeric: "233", alpha2: "EE", alpha3: "EST", calling: "+372", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Ethiopia", numeric: "231", alpha2: "ET", alpha3: "ETH", calling: "+251", currency: "ETB", continent: "AF" ),
IsoCountryInfo(name: "Falkland Islands (Malvinas)", numeric: "238", alpha2: "FK", alpha3: "FLK", calling: "+500", currency: "FKP", continent: "SA" ),
IsoCountryInfo(name: "Faroe Islands", numeric: "234", alpha2: "FO", alpha3: "FRO", calling: "+298", currency: "DKK", continent: "EU" ),
IsoCountryInfo(name: "Fiji", numeric: "242", alpha2: "FJ", alpha3: "FJI", calling: "+679", currency: "FJD", continent: "OC" ),
IsoCountryInfo(name: "Finland", numeric: "246", alpha2: "FI", alpha3: "FIN", calling: "+358", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "France", numeric: "250", alpha2: "FR", alpha3: "FRA", calling: "+33", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "French Guiana", numeric: "254", alpha2: "GF", alpha3: "GUF", calling: "+594", currency: "EUR", continent: "SA" ),
IsoCountryInfo(name: "French Polynesia", numeric: "258", alpha2: "PF", alpha3: "PYF", calling: "+689", currency: "XPF", continent: "OC" ),
IsoCountryInfo(name: "French Southern Territories", numeric: "260", alpha2: "TF", alpha3: "ATF", calling: "+689", currency: "EUR", continent: "AN" ),
IsoCountryInfo(name: "Gabon", numeric: "266", alpha2: "GA", alpha3: "GAB", calling: "+241", currency: "XAF", continent: "AF" ),
IsoCountryInfo(name: "Gambia", numeric: "270", alpha2: "GM", alpha3: "GMB", calling: "+220", currency: "GMD", continent: "AF" ),
IsoCountryInfo(name: "Georgia", numeric: "268", alpha2: "GE", alpha3: "GEO", calling: "+995", currency: "GEL", continent: "AS" ),
IsoCountryInfo(name: "Germany", numeric: "276", alpha2: "DE", alpha3: "DEU", calling: "+49", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Ghana", numeric: "288", alpha2: "GH", alpha3: "GHA", calling: "+233", currency: "GHS", continent: "AF" ),
IsoCountryInfo(name: "Gibraltar", numeric: "292", alpha2: "GI", alpha3: "GIB", calling: "+350", currency: "GIP", continent: "EU" ),
IsoCountryInfo(name: "Greece", numeric: "300", alpha2: "GR", alpha3: "GRC", calling: "+30", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Greenland", numeric: "304", alpha2: "GL", alpha3: "GRL", calling: "+299", currency: "DKK", continent: "NA" ),
IsoCountryInfo(name: "Grenada", numeric: "308", alpha2: "GD", alpha3: "GRD", calling: "+473", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Guadeloupe", numeric: "312", alpha2: "GP", alpha3: "GLP", calling: "+590", currency: "EUR", continent: "NA" ),
IsoCountryInfo(name: "Guam", numeric: "316", alpha2: "GU", alpha3: "GUM", calling: "+671", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Guatemala", numeric: "320", alpha2: "GT", alpha3: "GTM", calling: "+502", currency: "GTQ", continent: "NA" ),
IsoCountryInfo(name: "Guernsey", numeric: "831", alpha2: "GG", alpha3: "GGY", calling: "+1481", currency: "GGP", continent: "EU" ),
IsoCountryInfo(name: "Guinea", numeric: "324", alpha2: "GN", alpha3: "GIN", calling: "+225", currency: "GNF", continent: "AF" ),
IsoCountryInfo(name: "Guinea-Bissau", numeric: "624", alpha2: "GW", alpha3: "GNB", calling: "+245", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Guyana", numeric: "328", alpha2: "GY", alpha3: "GUY", calling: "+592", currency: "GYD", continent: "SA" ),
IsoCountryInfo(name: "Haiti", numeric: "332", alpha2: "HT", alpha3: "HTI", calling: "+509", currency: "HTG", continent: "NA" ),
IsoCountryInfo(name: "Heard Island and McDonald Islands", numeric: "334", alpha2: "HM", alpha3: "HMD", calling: "+61", currency: "AUD", continent: "AN" ),
IsoCountryInfo(name: "Holy See (Vatican City State)", numeric: "336", alpha2: "VA", alpha3: "VAT", calling: "+379", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Honduras", numeric: "340", alpha2: "HN", alpha3: "HND", calling: "+504", currency: "HNL", continent: "NA" ),
IsoCountryInfo(name: "Hong Kong", numeric: "344", alpha2: "HK", alpha3: "HKG", calling: "+852", currency: "HKD", continent: "AS" ),
IsoCountryInfo(name: "Hungary", numeric: "348", alpha2: "HU", alpha3: "HUN", calling: "+36", currency: "HUF", continent: "EU" ),
IsoCountryInfo(name: "Iceland", numeric: "352", alpha2: "IS", alpha3: "ISL", calling: "+354", currency: "ISK", continent: "EU" ),
IsoCountryInfo(name: "India", numeric: "356", alpha2: "IN", alpha3: "IND", calling: "+91", currency: "INR", continent: "AS" ),
IsoCountryInfo(name: "Indonesia", numeric: "360", alpha2: "ID", alpha3: "IDN", calling: "+62", currency: "IDR", continent: "AS" ),
IsoCountryInfo(name: "Iran, Islamic Republic of", numeric: "364", alpha2: "IR", alpha3: "IRN", calling: "+98", currency: "IRR", continent: "AS" ),
IsoCountryInfo(name: "Iraq", numeric: "368", alpha2: "IQ", alpha3: "IRQ", calling: "+964", currency: "IQD", continent: "AS" ),
IsoCountryInfo(name: "Ireland", numeric: "372", alpha2: "IE", alpha3: "IRL", calling: "+353", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Isle of Man", numeric: "833", alpha2: "IM", alpha3: "IMN", calling: "+44", currency: "IMP", continent: "EU" ),
IsoCountryInfo(name: "Israel", numeric: "376", alpha2: "IL", alpha3: "ISR", calling: "+972", currency: "ILS", continent: "AS" ),
IsoCountryInfo(name: "Italy", numeric: "380", alpha2: "IT", alpha3: "ITA", calling: "+39", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Jamaica", numeric: "388", alpha2: "JM", alpha3: "JAM", calling: "+876", currency: "JMD", continent: "NA" ),
IsoCountryInfo(name: "Japan", numeric: "392", alpha2: "JP", alpha3: "JPN", calling: "+81", currency: "JPY", continent: "AS" ),
IsoCountryInfo(name: "Jersey", numeric: "832", alpha2: "JE", alpha3: "JEY", calling: "+44", currency: "JEP", continent: "EU" ),
IsoCountryInfo(name: "Jordan", numeric: "400", alpha2: "JO", alpha3: "JOR", calling: "+962", currency: "JOD", continent: "AS" ),
IsoCountryInfo(name: "Kazakhstan", numeric: "398", alpha2: "KZ", alpha3: "KAZ", calling: "+7", currency: "KZT", continent: "AS" ),
IsoCountryInfo(name: "Kenya", numeric: "404", alpha2: "KE", alpha3: "KEN", calling: "+254", currency: "KES", continent: "AF" ),
IsoCountryInfo(name: "Kiribati", numeric: "296", alpha2: "KI", alpha3: "KIR", calling: "+686", currency: "AUD", continent: "OC" ),
IsoCountryInfo(name: "Korea, Democratic People's Republic of", numeric: "408", alpha2: "KP", alpha3: "PRK", calling: "+850", currency: "KPW", continent: "AS" ),
IsoCountryInfo(name: "Korea, Republic of", numeric: "410", alpha2: "KR", alpha3: "KOR", calling: "+82", currency: "KRW", continent: "AS" ),
IsoCountryInfo(name: "Kuwait", numeric: "414", alpha2: "KW", alpha3: "KWT", calling: "+965", currency: "KWD", continent: "AS" ),
IsoCountryInfo(name: "Kyrgyzstan", numeric: "417", alpha2: "KG", alpha3: "KGZ", calling: "+996", currency: "KGS", continent: "AS" ),
IsoCountryInfo(name: "Lao People's Democratic Republic", numeric: "418", alpha2: "LA", alpha3: "LAO", calling: "+856", currency: "LAK", continent: "AS" ),
IsoCountryInfo(name: "Latvia", numeric: "428", alpha2: "LV", alpha3: "LVA", calling: "+371", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Lebanon", numeric: "422", alpha2: "LB", alpha3: "LBN", calling: "+961", currency: "LBP", continent: "AS" ),
IsoCountryInfo(name: "Lesotho", numeric: "426", alpha2: "LS", alpha3: "LSO", calling: "+266", currency: "LSL", continent: "AF" ),
IsoCountryInfo(name: "Liberia", numeric: "430", alpha2: "LR", alpha3: "LBR", calling: "+231", currency: "LRD", continent: "AF" ),
IsoCountryInfo(name: "Libya", numeric: "434", alpha2: "LY", alpha3: "LBY", calling: "+218", currency: "LYD", continent: "AF" ),
IsoCountryInfo(name: "Liechtenstein", numeric: "438", alpha2: "LI", alpha3: "LIE", calling: "+423", currency: "CHF", continent: "EU" ),
IsoCountryInfo(name: "Lithuania", numeric: "440", alpha2: "LT", alpha3: "LTU", calling: "+370", currency: "LTL", continent: "EU" ),
IsoCountryInfo(name: "Luxembourg", numeric: "442", alpha2: "LU", alpha3: "LUX", calling: "+352", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Macao", numeric: "446", alpha2: "MO", alpha3: "MAC", calling: "+853", currency: "MOP", continent: "AS" ),
IsoCountryInfo(name: "Macedonia, the former Yugoslav Republic of", numeric: "807", alpha2: "MK", alpha3: "MKD", calling: "+389", currency: "MKD", continent: "EU" ),
IsoCountryInfo(name: "Madagascar", numeric: "450", alpha2: "MG", alpha3: "MDG", calling: "+261", currency: "MGA", continent: "AF" ),
IsoCountryInfo(name: "Malawi", numeric: "454", alpha2: "MW", alpha3: "MWI", calling: "+265", currency: "MWK", continent: "AF" ),
IsoCountryInfo(name: "Malaysia", numeric: "458", alpha2: "MY", alpha3: "MYS", calling: "+60", currency: "MYR", continent: "AS" ),
IsoCountryInfo(name: "Maldives", numeric: "462", alpha2: "MV", alpha3: "MDV", calling: "+960", currency: "MVR", continent: "AS" ),
IsoCountryInfo(name: "Mali", numeric: "466", alpha2: "ML", alpha3: "MLI", calling: "+223", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Malta", numeric: "470", alpha2: "MT", alpha3: "MLT", calling: "+356", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Marshall Islands", numeric: "584", alpha2: "MH", alpha3: "MHL", calling: "+692", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Martinique", numeric: "474", alpha2: "MQ", alpha3: "MTQ", calling: "+596", currency: "EUR", continent: "NA" ),
IsoCountryInfo(name: "Mauritania", numeric: "478", alpha2: "MR", alpha3: "MRT", calling: "+222", currency: "MRO", continent: "AF" ),
IsoCountryInfo(name: "Mauritius", numeric: "480", alpha2: "MU", alpha3: "MUS", calling: "+230", currency: "MUR", continent: "AF" ),
IsoCountryInfo(name: "Mayotte", numeric: "175", alpha2: "YT", alpha3: "MYT", calling: "+262", currency: "EUR", continent: "AF" ),
IsoCountryInfo(name: "Mexico", numeric: "484", alpha2: "MX", alpha3: "MEX", calling: "+52", currency: "MXN", continent: "NA" ),
IsoCountryInfo(name: "Micronesia, Federated States of", numeric: "583", alpha2: "FM", alpha3: "FSM", calling: "+691", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Moldova, Republic of", numeric: "498", alpha2: "MD", alpha3: "MDA", calling: "+373", currency: "MDL", continent: "EU" ),
IsoCountryInfo(name: "Monaco", numeric: "492", alpha2: "MC", alpha3: "MCO", calling: "+355", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Mongolia", numeric: "496", alpha2: "MN", alpha3: "MNG", calling: "+976", currency: "MNT", continent: "AS" ),
IsoCountryInfo(name: "Montenegro", numeric: "499", alpha2: "ME", alpha3: "MNE", calling: "+382", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Montserrat", numeric: "500", alpha2: "MS", alpha3: "MSR", calling: "+664", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Morocco", numeric: "504", alpha2: "MA", alpha3: "MAR", calling: "+212", currency: "MAD", continent: "AF" ),
IsoCountryInfo(name: "Mozambique", numeric: "508", alpha2: "MZ", alpha3: "MOZ", calling: "+258", currency: "MZN", continent: "AF" ),
IsoCountryInfo(name: "Myanmar", numeric: "104", alpha2: "MM", alpha3: "MMR", calling: "+95", currency: "MMK", continent: "AS" ),
IsoCountryInfo(name: "Namibia", numeric: "516", alpha2: "NA", alpha3: "NAM", calling: "+264", currency: "NAD", continent: "AF" ),
IsoCountryInfo(name: "Nauru", numeric: "520", alpha2: "NR", alpha3: "NRU", calling: "+674", currency: "AUD", continent: "OC" ),
IsoCountryInfo(name: "Nepal", numeric: "524", alpha2: "NP", alpha3: "NPL", calling: "+977", currency: "NPR", continent: "AS" ),
IsoCountryInfo(name: "Netherlands", numeric: "528", alpha2: "NL", alpha3: "NLD", calling: "+31", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "New Caledonia", numeric: "540", alpha2: "NC", alpha3: "NCL", calling: "+687", currency: "XPF", continent: "OC" ),
IsoCountryInfo(name: "New Zealand", numeric: "554", alpha2: "NZ", alpha3: "NZL", calling: "+64", currency: "NZD", continent: "OC" ),
IsoCountryInfo(name: "Nicaragua", numeric: "558", alpha2: "NI", alpha3: "NIC", calling: "+505", currency: "NIO", continent: "NA" ),
IsoCountryInfo(name: "Niger", numeric: "562", alpha2: "NE", alpha3: "NER", calling: "+277", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Nigeria", numeric: "566", alpha2: "NG", alpha3: "NGA", calling: "+234", currency: "NGN", continent: "AF" ),
IsoCountryInfo(name: "Niue", numeric: "570", alpha2: "NU", alpha3: "NIU", calling: "+683", currency: "NZD", continent: "OC" ),
IsoCountryInfo(name: "Norfolk Island", numeric: "574", alpha2: "NF", alpha3: "NFK", calling: "+672", currency: "AUD", continent: "OC" ),
IsoCountryInfo(name: "Northern Mariana Islands", numeric: "580", alpha2: "MP", alpha3: "MNP", calling: "+670", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Norway", numeric: "578", alpha2: "NO", alpha3: "NOR", calling: "+47", currency: "NOK", continent: "EU" ),
IsoCountryInfo(name: "Oman", numeric: "512", alpha2: "OM", alpha3: "OMN", calling: "+968", currency: "OMR", continent: "AS" ),
IsoCountryInfo(name: "Pakistan", numeric: "586", alpha2: "PK", alpha3: "PAK", calling: "+92", currency: "PKR", continent: "AS" ),
IsoCountryInfo(name: "Palau", numeric: "585", alpha2: "PW", alpha3: "PLW", calling: "+680", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Palestine, State of", numeric: "275", alpha2: "PS", alpha3: "PSE", calling: "+970", currency: "JOD", continent: "AS" ),
IsoCountryInfo(name: "Panama", numeric: "591", alpha2: "PA", alpha3: "PAN", calling: "+507", currency: "PAB", continent: "NA" ),
IsoCountryInfo(name: "Papua New Guinea", numeric: "598", alpha2: "PG", alpha3: "PNG", calling: "+675", currency: "PGK", continent: "OC" ),
IsoCountryInfo(name: "Paraguay", numeric: "600", alpha2: "PY", alpha3: "PRY", calling: "+595", currency: "PYG", continent: "SA" ),
IsoCountryInfo(name: "Peru", numeric: "604", alpha2: "PE", alpha3: "PER", calling: "+51", currency: "PEN", continent: "SA" ),
IsoCountryInfo(name: "Philippines", numeric: "608", alpha2: "PH", alpha3: "PHL", calling: "+63", currency: "PHP", continent: "AS" ),
IsoCountryInfo(name: "Pitcairn", numeric: "612", alpha2: "PN", alpha3: "PCN", calling: "+872", currency: "NZD", continent: "OC" ),
IsoCountryInfo(name: "Poland", numeric: "616", alpha2: "PL", alpha3: "POL", calling: "+48", currency: "PLN", continent: "EU" ),
IsoCountryInfo(name: "Portugal", numeric: "620", alpha2: "PT", alpha3: "PRT", calling: "+351", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Puerto Rico", numeric: "630", alpha2: "PR", alpha3: "PRI", calling: "+787", currency: "USD", continent: "NA" ),
IsoCountryInfo(name: "Qatar", numeric: "634", alpha2: "QA", alpha3: "QAT", calling: "+974", currency: "QAR", continent: "AS" ),
IsoCountryInfo(name: "Réunion", numeric: "638", alpha2: "RE", alpha3: "REU", calling: "+262", currency: "EUR", continent: "AF" ),
IsoCountryInfo(name: "Romania", numeric: "642", alpha2: "RO", alpha3: "ROU", calling: "+40", currency: "RON", continent: "EU" ),
IsoCountryInfo(name: "Russian Federation", numeric: "643", alpha2: "RU", alpha3: "RUS", calling: "+7", currency: "RUB", continent: "EU" ),
IsoCountryInfo(name: "Rwanda", numeric: "646", alpha2: "RW", alpha3: "RWA", calling: "+250", currency: "RWF", continent: "AF" ),
IsoCountryInfo(name: "Saint Barthélemy", numeric: "652", alpha2: "BL", alpha3: "BLM", calling: "+590", currency: "EUR", continent: "NA" ),
IsoCountryInfo(name: "Saint Helena, Ascension and Tristan da Cunha", numeric: "654", alpha2: "SH", alpha3: "SHN", calling: "+290", currency: "SHP", continent: "AF" ),
IsoCountryInfo(name: "Saint Kitts and Nevis", numeric: "659", alpha2: "KN", alpha3: "KNA", calling: "+869", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Saint Lucia", numeric: "662", alpha2: "LC", alpha3: "LCA", calling: "+758", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Saint Martin (French part)", numeric: "663", alpha2: "MF", alpha3: "MAF", calling: "+590", currency: "EUR", continent: "NA" ),
IsoCountryInfo(name: "Saint Pierre and Miquelon", numeric: "666", alpha2: "PM", alpha3: "SPM", calling: "+508", currency: "EUR", continent: "NA" ),
IsoCountryInfo(name: "Saint Vincent and the Grenadines", numeric: "670", alpha2: "VC", alpha3: "VCT", calling: "+784", currency: "XCD", continent: "NA" ),
IsoCountryInfo(name: "Samoa", numeric: "882", alpha2: "WS", alpha3: "WSM", calling: "+685", currency: "WST", continent: "OC" ),
IsoCountryInfo(name: "San Marino", numeric: "674", alpha2: "SM", alpha3: "SMR", calling: "+378", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Sao Tome and Principe", numeric: "678", alpha2: "ST", alpha3: "STP", calling: "+239", currency: "STD", continent: "AF" ),
IsoCountryInfo(name: "Saudi Arabia", numeric: "682", alpha2: "SA", alpha3: "SAU", calling: "+966", currency: "SAR", continent: "AS" ),
IsoCountryInfo(name: "Senegal", numeric: "686", alpha2: "SN", alpha3: "SEN", calling: "+221", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Serbia", numeric: "688", alpha2: "RS", alpha3: "SRB", calling: "+381", currency: "RSD", continent: "EU" ),
IsoCountryInfo(name: "Seychelles", numeric: "690", alpha2: "SC", alpha3: "SYC", calling: "+248", currency: "SCR", continent: "AF" ),
IsoCountryInfo(name: "Sierra Leone", numeric: "694", alpha2: "SL", alpha3: "SLE", calling: "+232", currency: "SLL", continent: "AF" ),
IsoCountryInfo(name: "Singapore", numeric: "702", alpha2: "SG", alpha3: "SGP", calling: "+65", currency: "SGD", continent: "AS" ),
IsoCountryInfo(name: "Sint Maarten (Dutch part)", numeric: "534", alpha2: "SX", alpha3: "SXM", calling: "+599", currency: "ANG", continent: "" ),
IsoCountryInfo(name: "Slovakia", numeric: "703", alpha2: "SK", alpha3: "SVK", calling: "+421", currency: "SKK", continent: "EU" ),
IsoCountryInfo(name: "Slovenia", numeric: "705", alpha2: "SI", alpha3: "SVN", calling: "+386", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Solomon Islands", numeric: "090", alpha2: "SB", alpha3: "SLB", calling: "+677", currency: "SBD", continent: "OC" ),
IsoCountryInfo(name: "Somalia", numeric: "706", alpha2: "SO", alpha3: "SOM", calling: "+252", currency: "SOS", continent: "AF" ),
IsoCountryInfo(name: "South Africa", numeric: "710", alpha2: "ZA", alpha3: "ZAF", calling: "+27", currency: "ZAR", continent: "AF" ),
IsoCountryInfo(name: "South Georgia and the South Sandwich Islands", numeric: "239", alpha2: "GS", alpha3: "SGS", calling: "+500", currency: "GBP", continent: "AN" ),
IsoCountryInfo(name: "South Sudan", numeric: "728", alpha2: "SS", alpha3: "SSD", calling: "+211", currency: "SSP", continent: "" ),
IsoCountryInfo(name: "Spain", numeric: "724", alpha2: "ES", alpha3: "ESP", calling: "+34", currency: "EUR", continent: "EU" ),
IsoCountryInfo(name: "Sri Lanka", numeric: "144", alpha2: "LK", alpha3: "LKA", calling: "+94", currency: "LKR", continent: "AS" ),
IsoCountryInfo(name: "Sudan", numeric: "729", alpha2: "SD", alpha3: "SDN", calling: "+249", currency: "SDG", continent: "AF" ),
IsoCountryInfo(name: "Suriname", numeric: "740", alpha2: "SR", alpha3: "SUR", calling: "+597", currency: "SRD", continent: "SA" ),
IsoCountryInfo(name: "Svalbard and Jan Mayen", numeric: "744", alpha2: "SJ", alpha3: "SJM", calling: "+47", currency: "NOK", continent: "EU" ),
IsoCountryInfo(name: "Swaziland", numeric: "748", alpha2: "SZ", alpha3: "SWZ", calling: "+268", currency: "CHF", continent: "AF" ),
IsoCountryInfo(name: "Sweden", numeric: "752", alpha2: "SE", alpha3: "SWE", calling: "+46", currency: "SEK", continent: "EU" ),
IsoCountryInfo(name: "Switzerland", numeric: "756", alpha2: "CH", alpha3: "CHE", calling: "+41", currency: "CHF", continent: "EU" ),
IsoCountryInfo(name: "Syrian Arab Republic", numeric: "760", alpha2: "SY", alpha3: "SYR", calling: "+963", currency: "SYP", continent: "AS" ),
IsoCountryInfo(name: "Taiwan, Province of China", numeric: "158", alpha2: "TW", alpha3: "TWN", calling: "+886", currency: "TWD", continent: "AS" ),
IsoCountryInfo(name: "Tajikistan", numeric: "762", alpha2: "TJ", alpha3: "TJK", calling: "+992", currency: "TJS", continent: "AS" ),
IsoCountryInfo(name: "Tanzania, United Republic of", numeric: "834", alpha2: "TZ", alpha3: "TZA", calling: "+255", currency: "TZS", continent: "AF" ),
IsoCountryInfo(name: "Thailand", numeric: "764", alpha2: "TH", alpha3: "THA", calling: "+66", currency: "THB", continent: "AS" ),
IsoCountryInfo(name: "Timor-Leste", numeric: "626", alpha2: "TL", alpha3: "TLS", calling: "+670", currency: "IDR", continent: "AS" ),
IsoCountryInfo(name: "Togo", numeric: "768", alpha2: "TG", alpha3: "TGO", calling: "+228", currency: "XOF", continent: "AF" ),
IsoCountryInfo(name: "Tokelau", numeric: "772", alpha2: "TK", alpha3: "TKL", calling: "+690", currency: "NZD", continent: "OC" ),
IsoCountryInfo(name: "Tonga", numeric: "776", alpha2: "TO", alpha3: "TON", calling: "+676", currency: "TOP", continent: "OC" ),
IsoCountryInfo(name: "Trinidad and Tobago", numeric: "780", alpha2: "TT", alpha3: "TTO", calling: "+868", currency: "TTD", continent: "NA" ),
IsoCountryInfo(name: "Tunisia", numeric: "788", alpha2: "TN", alpha3: "TUN", calling: "+216", currency: "TND", continent: "AF" ),
IsoCountryInfo(name: "Turkey", numeric: "792", alpha2: "TR", alpha3: "TUR", calling: "+90", currency: "TRY", continent: "EU" ),
IsoCountryInfo(name: "Turkmenistan", numeric: "795", alpha2: "TM", alpha3: "TKM", calling: "+993", currency: "TMM", continent: "AS" ),
IsoCountryInfo(name: "Turks and Caicos Islands", numeric: "796", alpha2: "TC", alpha3: "TCA", calling: "+649", currency: "USD", continent: "NA" ),
IsoCountryInfo(name: "Tuvalu", numeric: "798", alpha2: "TV", alpha3: "TUV", calling: "+688", currency: "TVD", continent: "OC" ),
IsoCountryInfo(name: "Uganda", numeric: "800", alpha2: "UG", alpha3: "UGA", calling: "+256", currency: "UGX", continent: "AF" ),
IsoCountryInfo(name: "Ukraine", numeric: "804", alpha2: "UA", alpha3: "UKR", calling: "+380", currency: "UAH", continent: "EU" ),
IsoCountryInfo(name: "United Arab Emirates", numeric: "784", alpha2: "AE", alpha3: "ARE", calling: "+971", currency: "AED", continent: "AS" ),
IsoCountryInfo(name: "United Kingdom", numeric: "826", alpha2: "GB", alpha3: "GBR", calling: "+44", currency: "GBP", continent: "EU" ),
IsoCountryInfo(name: "United States", numeric: "840", alpha2: "US", alpha3: "USA", calling: "+1", currency: "USD", continent: "NA" ),
IsoCountryInfo(name: "United States Minor Outlying Islands", numeric: "581", alpha2: "UM", alpha3: "UMI", calling: "+1", currency: "USD", continent: "OC" ),
IsoCountryInfo(name: "Uruguay", numeric: "858", alpha2: "UY", alpha3: "URY", calling: "+598", currency: "UYU", continent: "SA" ),
IsoCountryInfo(name: "Uzbekistan", numeric: "860", alpha2: "UZ", alpha3: "UZB", calling: "+998", currency: "UZS", continent: "AS" ),
IsoCountryInfo(name: "Vanuatu", numeric: "548", alpha2: "VU", alpha3: "VUT", calling: "+678", currency: "VUV", continent: "OC" ),
IsoCountryInfo(name: "Venezuela, Bolivarian Republic of", numeric: "862", alpha2: "VE", alpha3: "VEN", calling: "+58", currency: "VEF", continent: "SA" ),
IsoCountryInfo(name: "Vietnam", numeric: "704", alpha2: "VN", alpha3: "VNM", calling: "+84", currency: "VND", continent: "AS" ),
IsoCountryInfo(name: "Virgin Islands, British", numeric: "092", alpha2: "VG", alpha3: "VGB", calling: "+284", currency: "USD", continent: "NA" ),
IsoCountryInfo(name: "Virgin Islands, U.S.", numeric: "850", alpha2: "VI", alpha3: "VIR", calling: "+340", currency: "USD", continent: "NA" ),
IsoCountryInfo(name: "Wallis and Futuna", numeric: "876", alpha2: "WF", alpha3: "WLF", calling: "+681", currency: "XPF", continent: "OC" ),
IsoCountryInfo(name: "Western Sahara", numeric: "732", alpha2: "EH", alpha3: "ESH", calling: "+212", currency: "MAD", continent: "AF" ),
IsoCountryInfo(name: "Yemen", numeric: "887", alpha2: "YE", alpha3: "YEM", calling: "+967", currency: "YER", continent: "AS" ),
IsoCountryInfo(name: "Zambia", numeric: "894", alpha2: "ZM", alpha3: "ZMB", calling: "+260", currency: "ZMW", continent: "AF" ),
IsoCountryInfo(name: "Zimbabwe", numeric: "716", alpha2: "ZW", alpha3: "ZWE", calling: "+263", currency: "ZWD", continent: "AF" )]
}
}
}
@@ -0,0 +1,37 @@
import Foundation
import UIKit
import Display
import LegacyComponents
import TelegramPresentationData
import LegacyUI
func legacySecureIdScanController(theme: PresentationTheme, strings: PresentationStrings, finished: @escaping (SecureIdRecognizedDocumentData?) -> Void) -> ViewController {
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: theme, strings: strings)
let theme = TGPassportScanControllerTheme(backgroundColor: theme.list.plainBackgroundColor, textColor: theme.list.itemPrimaryTextColor)
let controller = TGPassportScanController(context: legacyController.context, theme: theme)!
controller.finishedWithMRZ = { value in
if let value = value {
var issuingCountry: String? = nil
if let issuingCountryValue = value.issuingCountry {
issuingCountry = countryCodeAlpha3ToAlpha2(issuingCountryValue)
}
var nationality: String? = nil
if let nationalityValue = value.nationality {
nationality = countryCodeAlpha3ToAlpha2(nationalityValue)
}
finished(SecureIdRecognizedDocumentData(documentType: value.documentType, documentSubtype: value.documentSubtype, issuingCountry: issuingCountry, nationality: nationality, lastName: value.lastName.capitalized, firstName: value.firstName.capitalized, documentNumber: value.documentNumber, birthDate: value.birthDate, gender: value.gender, expiryDate: value.expiryDate))
} else {
finished(nil)
}
}
let navigationController = TGNavigationController(controllers: [controller])!
controller.navigation_setDismiss({ [weak legacyController] in
legacyController?.dismiss()
}, rootController: nil)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
legacyController.bind(controller: navigationController)
return legacyController
}
@@ -0,0 +1,93 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
private let textFont: UIFont = Font.regular(16.0)
final class SecureIdAuthAcceptNode: ASDisplayNode {
private let separatorNode: ASDisplayNode
private let buttonBackgroundNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let iconNode: ASImageNode
private let labelNode: ImmediateTextNode
var pressed: (() -> Void)?
init(title: String, theme: PresentationTheme) {
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
self.buttonBackgroundNode = ASImageNode()
self.buttonBackgroundNode.isLayerBacked = true
self.buttonBackgroundNode.displayWithoutProcessing = true
self.buttonBackgroundNode.displaysAsynchronously = false
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: 24.0, color: theme.list.itemCheckColors.fillColor)
self.buttonNode = HighlightTrackingButtonNode()
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/GrantIcon"), color: theme.list.itemCheckColors.foregroundColor)
self.labelNode = ImmediateTextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.list.itemCheckColors.foregroundColor)
super.init()
self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonBackgroundNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.labelNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.55
} else {
strongSelf.buttonBackgroundNode.alpha = 1.0
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
}
}
}
func updateLayout(width: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
let baseHeight: CGFloat = 78.0
let buttonSize = CGSize(width: width - 16.0 * 2.0, height: 48.0)
let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((baseHeight - buttonSize.height) / 2.0)), size: buttonSize)
transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame)
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
let labelSize = self.labelNode.updateLayout(buttonSize)
var labelFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - labelSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.iconNode.image {
labelFrame.origin.x += 4.0
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: labelFrame.minX - image.size.width - 7.0, y: labelFrame.minY - 1.0), size: image.size))
}
transition.updateFrame(node: self.labelNode, frame: labelFrame)
return baseHeight + bottomInset
}
@objc private func buttonPressed() {
self.pressed?()
}
}
@@ -0,0 +1,17 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
struct SecureIdAuthContentLayout {
let height: CGFloat
let centerOffset: CGFloat
}
protocol SecureIdAuthContentNode {
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout
func willDisappear()
func didAppear()
func animateIn()
func animateOut(completion: @escaping () -> Void)
}
@@ -0,0 +1,671 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TextFormat
import ProgressNavigationButtonNode
import AccountContext
import AlertUI
import PresentationDataUtils
import PasswordSetupUI
public enum SecureIdRequestResult: String {
case success = "success"
case cancel = "cancel"
case error = "error"
}
public func secureIdCallbackUrl(with baseUrl: String, peerId: PeerId, result: SecureIdRequestResult, parameters: [String : String]) -> String {
var query = (parameters.compactMap({ (key, value) -> String in
return "\(key)=\(value)"
}) as Array).joined(separator: "&")
if !query.isEmpty {
query = "?" + query
}
let url: String
if baseUrl.hasPrefix("tgbot") {
url = "tgbot\(peerId.id)://passport/" + result.rawValue + query
} else {
url = baseUrl + (baseUrl.range(of: "?") != nil ? "&" : "?") + "tg_passport=" + result.rawValue + query
}
return url
}
final class SecureIdAuthControllerInteraction {
let updateState: ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void
let present: (ViewController, Any?) -> Void
let push: (ViewController) -> Void
let checkPassword: (String) -> Void
let openPasswordHelp: () -> Void
let setupPassword: () -> Void
let grant: () -> Void
let openUrl: (String) -> Void
let openMention: (TelegramPeerMention) -> Void
let deleteAll: () -> Void
fileprivate init(updateState: @escaping ((SecureIdAuthControllerState) -> SecureIdAuthControllerState) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, checkPassword: @escaping (String) -> Void, openPasswordHelp: @escaping () -> Void, setupPassword: @escaping () -> Void, grant: @escaping () -> Void, openUrl: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void, deleteAll: @escaping () -> Void) {
self.updateState = updateState
self.present = present
self.push = push
self.checkPassword = checkPassword
self.openPasswordHelp = openPasswordHelp
self.setupPassword = setupPassword
self.grant = grant
self.openUrl = openUrl
self.openMention = openMention
self.deleteAll = deleteAll
}
}
public enum SecureIdAuthControllerMode {
case form(peerId: PeerId, scope: String, publicKey: String, callbackUrl: String?, opaquePayload: Data, opaqueNonce: Data)
case list
}
public final class SecureIdAuthController: ViewController, StandalonePresentableController {
private var controllerNode: SecureIdAuthControllerNode {
return self.displayNode as! SecureIdAuthControllerNode
}
private let context: AccountContext
private var presentationData: PresentationData
private let mode: SecureIdAuthControllerMode
private var didPlayPresentationAnimation = false
private let challengeDisposable = MetaDisposable()
private let authenthicateDisposable = MetaDisposable()
private var formDisposable: Disposable?
private let deleteDisposable = MetaDisposable()
private let recoveryDisposable = MetaDisposable()
private var state: SecureIdAuthControllerState
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, mode: SecureIdAuthControllerMode) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.mode = mode
switch mode {
case .form:
self.state = .form(SecureIdAuthControllerFormState(twoStepEmail: nil, encryptedFormData: nil, formData: nil, verificationState: nil, removingValues: false))
case .list:
self.state = .list(SecureIdAuthControllerListState(accountPeer: nil, twoStepEmail: nil, verificationState: nil, encryptedValues: nil, primaryLanguageByCountry: [:], values: nil, removingValues: false))
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
if case .list = mode {
self.navigationPresentation = .modal
}
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.Passport_Title
switch mode {
case .form:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
case .list:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.cancelPressed))
}
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.infoPressed))
self.challengeDisposable.set((context.engine.auth.twoStepAuthData()
|> deliverOnMainQueue).start(next: { [weak self] data in
if let strongSelf = self {
let storedPassword = context.getStoredSecureIdPassword()
if data.currentPasswordDerivation != nil, let storedPassword = storedPassword {
strongSelf.authenthicateDisposable.set((strongSelf.context.engine.secureId.accessSecureId(password: storedPassword)
|> deliverOnMainQueue).start(next: { context in
guard let strongSelf = self, strongSelf.state.verificationState == nil else {
return
}
strongSelf.updateState(animated: true, { state in
var state = state
state.verificationState = .verified(context.context)
state.twoStepEmail = !context.settings.email.isEmpty ? context.settings.email : nil
switch state {
case var .form(form):
form.formData = form.encryptedFormData.flatMap({ decryptedSecureIdForm(context: context.context, form: $0.form) })
state = .form(form)
case var .list(list):
list.values = list.encryptedValues.flatMap({ decryptedAllSecureIdValues(context: context.context, encryptedValues: $0) })
state = .list(list)
}
return state
})
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
if strongSelf.state.verificationState == nil {
strongSelf.updateState(animated: true, { state in
var state = state
state.verificationState = .passwordChallenge(hint: data.currentHint ?? "", state: .none, hasRecoveryEmail: data.hasRecovery)
return state
})
}
}))
} else {
strongSelf.updateState { state in
var state = state
if data.currentPasswordDerivation != nil {
state.verificationState = .passwordChallenge(hint: data.currentHint ?? "", state: .none, hasRecoveryEmail: data.hasRecovery)
} else if let unconfirmedEmailPattern = data.unconfirmedEmailPattern {
state.verificationState = .noChallenge(.awaitingConfirmation(password: nil, emailPattern: unconfirmedEmailPattern, codeLength: nil))
} else {
state.verificationState = .noChallenge(.notSet)
}
return state
}
}
}
}))
let handleError: (Any, String?, PeerId?) -> Void = { [weak self] error, callbackUrl, peerId in
if let strongSelf = self {
var passError: String?
var appUpdateRequired = false
switch error {
case let error as RequestSecureIdFormError:
if case let .serverError(error) = error, ["BOT_INVALID", "PUBLIC_KEY_REQUIRED", "PUBLIC_KEY_INVALID", "SCOPE_EMPTY", "PAYLOAD_EMPTY", "NONCE_EMPTY"].contains(error) {
passError = error
} else if case .versionOutdated = error {
appUpdateRequired = true
}
break
case let error as GetAllSecureIdValuesError:
if case .versionOutdated = error {
appUpdateRequired = true
}
break
default:
break
}
if appUpdateRequired {
let errorText = strongSelf.presentationData.strings.Passport_UpdateRequiredError
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Application_Update, action: {
context.sharedContext.applicationBindings.openAppStorePage()
})]), in: .window(.root))
} else if let callbackUrl = callbackUrl, let peerId = peerId {
let errorText = strongSelf.presentationData.strings.Login_UnknownError
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
if let error = passError {
strongSelf.openUrl(secureIdCallbackUrl(with: callbackUrl, peerId: peerId, result: .error, parameters: ["error": error]))
}
})]), in: .window(.root))
}
strongSelf.dismiss()
}
}
switch self.mode {
case let .form(peerId, scope, publicKey, callbackUrl, _, _):
self.formDisposable = (combineLatest(requestSecureIdForm(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network, peerId: peerId, scope: scope, publicKey: publicKey), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(RequestSecureIdFormError.self))
|> mapToSignal { form, configuration -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: form.peerId)
)
|> castError(RequestSecureIdFormError.self)
|> mapToSignal { accountPeer, servicePeer -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
guard let accountPeer = accountPeer, let servicePeer = servicePeer else {
return .fail(.generic)
}
let primaryLanguageByCountry = configuration.nativeLanguageByCountry
return .single(SecureIdEncryptedFormData(form: form, primaryLanguageByCountry: primaryLanguageByCountry, accountPeer: accountPeer._asPeer(), servicePeer: servicePeer._asPeer()))
}
}
|> deliverOnMainQueue).start(next: { [weak self] formData in
if let strongSelf = self {
strongSelf.updateState { state in
var state = state
switch state {
case var .form(form):
form.encryptedFormData = formData
state = .form(form)
case .list:
break
}
return state
}
}
}, error: { error in
handleError(error, callbackUrl, peerId)
})
case .list:
self.formDisposable = (combineLatest(
getAllSecureIdValues(network: self.context.account.network),
secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(GetAllSecureIdValuesError.self),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> castError(GetAllSecureIdValuesError.self) |> mapToSignal { accountPeer -> Signal<EnginePeer, GetAllSecureIdValuesError> in
guard let accountPeer = accountPeer else {
return .fail(.generic)
}
return .single(accountPeer)
}
)
|> deliverOnMainQueue).start(next: { [weak self] values, configuration, accountPeer in
if let strongSelf = self {
strongSelf.updateState { state in
let state = state
let primaryLanguageByCountry = configuration.nativeLanguageByCountry
switch state {
case .form:
break
case var .list(list):
list.accountPeer = accountPeer._asPeer()
list.primaryLanguageByCountry = primaryLanguageByCountry
list.encryptedValues = values
return .list(list)
}
return state
}
}
}, error: { error in
handleError(error, nil, nil)
})
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.challengeDisposable.dispose()
self.authenthicateDisposable.dispose()
self.formDisposable?.dispose()
self.deleteDisposable.dispose()
self.recoveryDisposable.dispose()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if case .form = self.mode, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
self.controllerNode.animateIn()
}
}
override public func loadDisplayNode() {
let interaction = SecureIdAuthControllerInteraction(updateState: { [weak self] f in
self?.updateState(f)
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, push: { [weak self] c in
self?.push(c)
}, checkPassword: { [weak self] password in
self?.checkPassword(password: password, inBackground: false, completion: {})
}, openPasswordHelp: { [weak self] in
self?.openPasswordHelp()
}, setupPassword: { [weak self] in
self?.setupPassword()
}, grant: { [weak self] in
self?.grantAccess()
}, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: false, presentationData: strongSelf.presentationData, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: {
self?.view.endEditing(true)
})
}
}, openMention: { [weak self] mention in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(mention.peerId)
|> deliverOnMainQueue).start(next: { peer in
guard let strongSelf = self else {
return
}
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
}
})
}, deleteAll: { [weak self] in
guard let strongSelf = self, case let .list(list) = strongSelf.state, let values = list.values else {
return
}
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.presentationData.theme.rootController.navigationBar.controlColor))
strongSelf.navigationItem.rightBarButtonItem = item
strongSelf.deleteDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set(values.map({ $0.value.key })))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.infoPressed))
strongSelf.updateState { state in
if case var .list(list) = state {
list.values = []
return .list(list)
}
return state
}
}))
})
self.displayNode = SecureIdAuthControllerNode(context: self.context, presentationData: presentationData, requestLayout: { [weak self] transition in
self?.requestLayout(transition: transition)
}, interaction: interaction)
self.controllerNode.updateState(self.state, transition: .immediate)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
if case .form = self.mode {
self.controllerNode.animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
private func updateState(animated: Bool = true, _ f: (SecureIdAuthControllerState) -> SecureIdAuthControllerState) {
let state = f(self.state)
if state != self.state {
var previousHadProgress = false
if let verificationState = self.state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState {
previousHadProgress = true
}
if self.state.removingValues {
previousHadProgress = true
}
var updatedHasProgress = false
if let verificationState = state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState {
updatedHasProgress = true
}
if state.removingValues {
updatedHasProgress = true
}
self.state = state
if self.isNodeLoaded {
self.controllerNode.updateState(self.state, transition: animated ? .animated(duration: 0.3, curve: .spring) : .immediate)
}
if previousHadProgress != updatedHasProgress {
if updatedHasProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.controlColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationInfoIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.infoPressed))
}
}
}
}
private func openUrl(_ url: String) {
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: { [weak self] in
self?.view.endEditing(true)
})
}
@objc private func cancelPressed() {
self.dismiss()
if case let .form(peerId, _, _, maybeCallbackUrl, _, _) = self.mode, let callbackUrl = maybeCallbackUrl {
self.openUrl(secureIdCallbackUrl(with: callbackUrl, peerId: peerId, result: .cancel, parameters: [:]))
}
}
@objc private func checkPassword(password: String, inBackground: Bool, completion: @escaping () -> Void) {
if let verificationState = self.state.verificationState, case let .passwordChallenge(hint, challengeState, hasRecoveryEmail) = verificationState {
switch challengeState {
case .none, .invalid:
break
case .checking:
return
}
self.updateState(animated: !inBackground, { state in
var state = state
state.verificationState = .passwordChallenge(hint: hint, state: .checking, hasRecoveryEmail: hasRecoveryEmail)
return state
})
self.challengeDisposable.set((self.context.engine.secureId.accessSecureId(password: password)
|> deliverOnMainQueue).start(next: { [weak self] context in
guard let strongSelf = self, let verificationState = strongSelf.state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState else {
return
}
strongSelf.context.storeSecureIdPassword(password: password)
strongSelf.updateState(animated: !inBackground, { state in
var state = state
state.verificationState = .verified(context.context)
state.twoStepEmail = !context.settings.email.isEmpty ? context.settings.email : nil
switch state {
case var .form(form):
form.formData = form.encryptedFormData.flatMap({ decryptedSecureIdForm(context: context.context, form: $0.form) })
state = .form(form)
case var .list(list):
list.values = list.encryptedValues.flatMap({ decryptedAllSecureIdValues(context: context.context, encryptedValues: $0) })
state = .list(list)
}
return state
})
completion()
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
let errorText: String
switch error {
case let .passwordError(passwordError):
switch passwordError {
case .invalidPassword:
errorText = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError
case .limitExceeded:
errorText = strongSelf.presentationData.strings.LoginPassword_FloodError
case .generic:
errorText = strongSelf.presentationData.strings.Login_UnknownError
}
case .generic:
errorText = strongSelf.presentationData.strings.Login_UnknownError
case .secretPasswordMismatch:
errorText = strongSelf.presentationData.strings.Login_UnknownError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
if let verificationState = strongSelf.state.verificationState, case let .passwordChallenge(hint, .checking, hasRecoveryEmail) = verificationState {
strongSelf.updateState(animated: !inBackground, { state in
var state = state
state.verificationState = .passwordChallenge(hint: hint, state: .invalid, hasRecoveryEmail: hasRecoveryEmail)
return state
})
}
completion()
}))
}
}
private func openPasswordHelp() {
guard let verificationState = self.state.verificationState, case let .passwordChallenge(_, state, hasRecoveryEmail) = verificationState else {
return
}
switch state {
case .checking:
return
case .none, .invalid:
break
}
if hasRecoveryEmail {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.Passport_ForgottenPassword, text: self.presentationData.strings.Passport_PasswordReset, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_ResetAccountProtected_Reset, action: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.recoveryDisposable.set((strongSelf.context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode()
|> deliverOnMainQueue).start(next: { emailPattern in
guard let strongSelf = self else {
return
}
var completionImpl: (() -> Void)?
let controller = resetPasswordController(context: strongSelf.context, emailPattern: emailPattern, completion: { _ in
completionImpl?()
})
completionImpl = { [weak controller] in
guard let strongSelf = self else {
return
}
strongSelf.updateState(animated: false, { state in
var state = state
state.verificationState = .noChallenge(.notSet)
return state
})
controller?.view.endEditing(true)
controller?.dismiss()
strongSelf.setupPassword()
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
})]), in: .window(.root))
} else {
self.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
private func setupPassword() {
guard let verificationState = self.state.verificationState, case let .noChallenge(noChallengeState) = verificationState else {
return
}
let initialState: SetupTwoStepVerificationInitialState
switch noChallengeState {
case .notSet:
initialState = .createPassword
case let .awaitingConfirmation(password, emailPattern, codeLength):
initialState = .confirmEmail(password: password, hasSecureValues: false, pattern: emailPattern, codeLength: codeLength)
}
let controller = SetupTwoStepVerificationController(context: self.context, initialState: initialState, stateUpdated: { [weak self] update, shouldDismiss, controller in
guard let strongSelf = self else {
return
}
switch update {
case .noPassword, .pendingPasswordReset:
strongSelf.updateState(animated: false, { state in
var state = state
if let verificationState = state.verificationState, case .noChallenge = verificationState {
state.verificationState = .noChallenge(.notSet)
}
return state
})
if shouldDismiss {
controller.dismiss()
}
case let .awaitingEmailConfirmation(password, pattern, codeLength):
strongSelf.updateState(animated: false, { state in
var state = state
if let verificationState = state.verificationState, case .noChallenge = verificationState {
state.verificationState = .noChallenge(.awaitingConfirmation(password: password, emailPattern: pattern, codeLength: codeLength))
}
return state
})
if shouldDismiss {
controller.dismiss()
}
case let .passwordSet(password, hasRecoveryEmail, _):
strongSelf.updateState(animated: false, { state in
var state = state
state.verificationState = .passwordChallenge(hint: "", state: .none, hasRecoveryEmail: hasRecoveryEmail)
return state
})
if let password = password {
strongSelf.checkPassword(password: password, inBackground: true, completion: { [weak controller] in
controller?.dismiss()
})
} else if shouldDismiss {
controller.dismiss()
}
}
})
self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
/*var completionImpl: ((String, String, Bool) -> Void)?
let state: CreatePasswordState
if let emailPattern = emailPattern {
state = .pendingVerification(emailPattern: emailPattern)
} else {
state = .setup(currentPassword: nil)
}
let controller = createPasswordController(account: self.account, context: .secureId, state: state, completion: { password, hint, hasRecoveryEmail in
completionImpl?(password, hint, hasRecoveryEmail)
}, updatePasswordEmailConfirmation: { [weak self] pattern in
guard let strongSelf = self else {
return
}
strongSelf.updateState(animated: false, { state in
var state = state
if let verificationState = state.verificationState, case .noChallenge = verificationState {
state.verificationState = .noChallenge(pattern?.1)
}
return state
})
})
completionImpl = { [weak self, weak controller] password, hint, hasRecoveryEmail in
guard let strongSelf = self else {
controller?.dismiss()
return
}
strongSelf.updateState(animated: false, { state in
var state = state
state.verificationState = .passwordChallenge(hint: hint, state: .none, hasRecoveryEmail: hasRecoveryEmail)
return state
})
strongSelf.checkPassword(password: password, inBackground: true, completion: {
controller?.view.endEditing(true)
controller?.dismiss()
})
}
self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/
}
@objc private func grantAccess() {
switch self.state {
case let .form(form):
if case let .form(peerId, scope, publicKey, callbackUrl, opaquePayload, opaqueNonce) = self.mode, let encryptedFormData = form.encryptedFormData, let formData = form.formData {
let values = parseRequestedFormFields(formData.requestedFields, values: formData.values, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry).map({ $0.1 }).flatMap({ $0 })
let _ = (grantSecureIdAccess(network: self.context.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: publicKey, scope: scope, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce, values: values, requestedFields: formData.requestedFields)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.dismiss()
if let callbackUrl = callbackUrl {
self?.openUrl(secureIdCallbackUrl(with: callbackUrl, peerId: peerId, result: .success, parameters: [:]))
}
})
}
case .list:
break
}
}
@objc private func infoPressed() {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.Passport_InfoTitle, text: self.presentationData.strings.Passport_InfoText.replacingOccurrences(of: "**", with: ""), actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Passport_InfoLearnMore, action: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: strongSelf.presentationData.strings.Passport_InfoFAQ_URL, forceExternal: false, presentationData: strongSelf.presentationData, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: {
self?.view.endEditing(true)
})
})]), in: .window(.root))
}
}
@@ -0,0 +1,980 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ActivityIndicator
import AccountContext
final class SecureIdAuthControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let requestLayout: (ContainedViewLayoutTransition) -> Void
private let interaction: SecureIdAuthControllerInteraction
private var hapticFeedback: HapticFeedback?
private var validLayout: (ContainerViewLayout, CGFloat)?
private let activityIndicator: ActivityIndicator
private let scrollNode: ASScrollNode
private let headerNode: SecureIdAuthHeaderNode
private var contentNode: (ASDisplayNode & SecureIdAuthContentNode)?
private var dismissedContentNode: (ASDisplayNode & SecureIdAuthContentNode)?
private let acceptNode: SecureIdAuthAcceptNode
private var scheduledLayoutTransitionRequestId: Int = 0
private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)?
private var state: SecureIdAuthControllerState?
private let deleteValueDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, interaction: SecureIdAuthControllerInteraction) {
self.context = context
self.presentationData = presentationData
self.requestLayout = requestLayout
self.interaction = interaction
self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.freeMonoIconColor, 22.0, 2.0, false))
self.activityIndicator.isHidden = true
self.scrollNode = ASScrollNode()
self.headerNode = SecureIdAuthHeaderNode(context: context, theme: presentationData.theme, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)
self.acceptNode = SecureIdAuthAcceptNode(title: presentationData.strings.Passport_Authorize, theme: presentationData.theme)
super.init()
self.addSubnode(self.activityIndicator)
self.scrollNode.view.alwaysBounceVertical = true
self.addSubnode(self.scrollNode)
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.acceptNode.pressed = { [weak self] in
guard let strongSelf = self, let state = strongSelf.state, case let .form(form) = state, let encryptedFormData = form.encryptedFormData, let formData = form.formData else {
return
}
for (field, _, filled) in parseRequestedFormFields(formData.requestedFields, values: formData.values, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry) {
if !filled {
if let contentNode = strongSelf.contentNode as? SecureIdAuthFormContentNode {
if let rect = contentNode.frameForField(field) {
let subRect = contentNode.view.convert(rect, to: strongSelf.scrollNode.view)
strongSelf.scrollNode.view.scrollRectToVisible(subRect, animated: true)
}
contentNode.highlightField(field)
}
if strongSelf.hapticFeedback == nil {
strongSelf.hapticFeedback = HapticFeedback()
}
strongSelf.hapticFeedback?.error()
return
}
}
strongSelf.interaction.grant()
}
}
deinit {
self.deleteValueDisposable.dispose()
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.isDisappearing = true
self.view.endEditing(true)
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
private var isDisappearing = false
private var previousHeaderNodeAlpha: CGFloat = 0.0
private var hadContentNode = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
if self.isDisappearing {
return
}
let previousHadContentNode = self.hadContentNode
self.hadContentNode = self.contentNode != nil
var insetOptions: ContainerViewLayoutInsetOptions = []
if self.contentNode is SecureIdAuthPasswordOptionContentNode {
insetOptions.insert(.input)
}
var insets = layout.insets(options: insetOptions)
insets.bottom = max(insets.bottom, layout.safeInsets.bottom)
let activitySize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - activitySize.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - activitySize.height) / 2.0)), size: activitySize))
var headerNodeTransition: ContainedViewLayoutTransition = self.headerNode.bounds.height.isZero ? .immediate : transition
if self.previousHeaderNodeAlpha.isZero && !self.headerNode.alpha.isZero {
headerNodeTransition = .immediate
}
self.previousHeaderNodeAlpha = self.headerNode.alpha
let headerLayout: (compact: CGFloat, expanded: CGFloat, apply: (Bool) -> Void)
if self.headerNode.alpha.isZero {
headerLayout = (0.0, 0.0, { _ in })
} else {
headerLayout = self.headerNode.updateLayout(width: layout.size.width, transition: headerNodeTransition)
}
let acceptHeight = self.acceptNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
var footerHeight: CGFloat = 0.0
var contentSpacing: CGFloat
var acceptNodeTransition = transition
if !previousHadContentNode {
acceptNodeTransition = .immediate
}
acceptNodeTransition.updateFrame(node: self.acceptNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - acceptHeight), size: CGSize(width: layout.size.width, height: acceptHeight)))
var minContentSpacing: CGFloat = 10.0
if self.acceptNode.supernode != nil {
footerHeight += (acceptHeight - layout.intrinsicInsets.bottom)
contentSpacing = 25.0
minContentSpacing = 25.0
} else {
if self.contentNode is SecureIdAuthListContentNode {
contentSpacing = 16.0
} else if self.contentNode is SecureIdAuthPasswordSetupContentNode {
contentSpacing = 24.0
} else {
contentSpacing = 56.0
}
}
insets.bottom += footerHeight
let wrappingContentRect = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - insets.bottom - navigationBarHeight))
let contentRect = CGRect(origin: CGPoint(), size: wrappingContentRect.size)
transition.updateFrame(node: self.scrollNode, frame: wrappingContentRect)
if let contentNode = self.contentNode {
let contentFirstTime = contentNode.bounds.isEmpty
let contentNodeTransition: ContainedViewLayoutTransition = contentFirstTime ? .immediate : transition
let contentLayout = contentNode.updateLayout(width: layout.size.width, transition: contentNodeTransition)
let headerHeight: CGFloat
if self.contentNode is SecureIdAuthPasswordOptionContentNode && headerLayout.expanded + contentLayout.height + minContentSpacing + 14.0 + 16.0 > contentRect.height {
headerHeight = headerLayout.compact
headerLayout.apply(false)
} else {
headerHeight = headerLayout.expanded
headerLayout.apply(true)
}
contentSpacing = max(minContentSpacing, min(contentSpacing, contentRect.height - (headerHeight + contentLayout.height + minContentSpacing + 14.0 + 16.0)))
let boundingHeight = headerHeight + contentLayout.height + contentSpacing
var boundingRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: boundingHeight))
if contentNode is SecureIdAuthListContentNode {
boundingRect.origin.y = contentRect.minY
} else {
boundingRect.origin.y = contentRect.minY + floor((contentRect.height - boundingHeight) / 2.0)
}
boundingRect.origin.y = max(boundingRect.origin.y, 14.0)
if self.headerNode.alpha.isZero {
headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: -boundingRect.width, y: self.headerNode.frame.minY), size: CGSize(width: boundingRect.width, height: headerHeight)))
} else {
headerNodeTransition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY), size: CGSize(width: boundingRect.width, height: headerHeight)))
}
contentNodeTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: boundingRect.minY + headerHeight + contentSpacing), size: CGSize(width: boundingRect.width, height: contentLayout.height)))
if contentFirstTime {
contentNode.didAppear()
if transition.isAnimated {
contentNode.animateIn()
if !(contentNode is SecureIdAuthPasswordOptionContentNode || contentNode is SecureIdAuthPasswordSetupContentNode) && previousHadContentNode {
transition.animatePositionAdditive(node: contentNode, offset: CGPoint(x: layout.size.width, y: 0.0))
}
}
}
self.scrollNode.view.contentSize = CGSize(width: boundingRect.width, height: 14.0 + boundingRect.height + 16.0)
}
if let dismissedContentNode = self.dismissedContentNode {
self.dismissedContentNode = nil
transition.updatePosition(node: dismissedContentNode, position: CGPoint(x: -layout.size.width / 2.0, y: dismissedContentNode.position.y), completion: { [weak dismissedContentNode] _ in
dismissedContentNode?.removeFromSupernode()
})
}
}
func transitionToContentNode(_ contentNode: (ASDisplayNode & SecureIdAuthContentNode)?, transition: ContainedViewLayoutTransition) {
if let current = self.contentNode {
current.willDisappear()
if let dismissedContentNode = self.dismissedContentNode, dismissedContentNode !== current {
dismissedContentNode.removeFromSupernode()
}
self.dismissedContentNode = current
}
self.contentNode = contentNode
if let contentNode = self.contentNode {
self.scrollNode.addSubnode(contentNode)
if let _ = self.validLayout {
if transition.isAnimated {
self.scheduleLayoutTransitionRequest(.animated(duration: 0.5, curve: .spring))
} else {
self.scheduleLayoutTransitionRequest(.immediate)
}
}
}
}
func updateState(_ state: SecureIdAuthControllerState, transition: ContainedViewLayoutTransition) {
self.state = state
var displayActivity = false
switch state {
case let .form(form):
if let encryptedFormData = form.encryptedFormData, let verificationState = form.verificationState {
if self.headerNode.supernode == nil {
self.scrollNode.addSubnode(self.headerNode)
self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.headerNode.updateState(formData: encryptedFormData, verificationState: verificationState)
var contentNode: (ASDisplayNode & SecureIdAuthContentNode)?
switch verificationState {
case let .noChallenge(noChallengeState):
if let _ = self.contentNode as? SecureIdAuthPasswordSetupContentNode {
} else {
let current = SecureIdAuthPasswordSetupContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, setupPassword: { [weak self] in
self?.interaction.setupPassword()
})
contentNode = current
}
switch noChallengeState {
case .notSet:
(self.contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(false)
(contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(false)
case .awaitingConfirmation:
(self.contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(true)
(contentNode as? SecureIdAuthPasswordSetupContentNode)?.updatePendingConfirmation(true)
}
case let .passwordChallenge(hint, challengeState, _):
if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode {
current.updateIsChecking(challengeState == .checking)
if case .invalid = challengeState {
current.updateIsInvalid()
}
contentNode = current
} else {
let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in
if let strongSelf = self {
strongSelf.interaction.checkPassword(password)
}
}, passwordHelp: { [weak self] in
self?.interaction.openPasswordHelp()
})
current.updateIsChecking(challengeState == .checking)
if case .invalid = challengeState {
current.updateIsInvalid()
}
contentNode = current
}
case .verified:
if let encryptedFormData = form.encryptedFormData, let formData = form.formData {
if let current = self.contentNode as? SecureIdAuthFormContentNode {
current.updateValues(formData.values)
contentNode = current
} else {
let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: EnginePeer(encryptedFormData.servicePeer), privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in
if let strongSelf = self {
switch field {
case .identity, .address:
strongSelf.presentDocumentSelection(field: field)
case .phone:
strongSelf.presentPlaintextSelection(type: .phone)
case .email:
strongSelf.presentPlaintextSelection(type: .email)
}
}
}, openURL: { [weak self] url in
self?.interaction.openUrl(url)
}, openMention: { [weak self] mention in
self?.interaction.openMention(mention)
}, requestLayout: { [weak self] in
if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
})
contentNode = current
}
}
}
if case .verified = verificationState {
if self.acceptNode.supernode == nil {
self.addSubnode(self.acceptNode)
if transition.isAnimated {
self.acceptNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.acceptNode.bounds.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
}
if self.contentNode !== contentNode {
self.transitionToContentNode(contentNode, transition: transition)
}
} else {
displayActivity = true
}
case let .list(list):
if let _ = list.encryptedValues, let verificationState = list.verificationState {
if case .verified = verificationState {
if !self.headerNode.alpha.isZero {
self.headerNode.alpha = 0.0
self.headerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
} else {
if self.headerNode.supernode == nil {
self.scrollNode.addSubnode(self.headerNode)
self.headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.headerNode.updateState(formData: nil, verificationState: verificationState)
}
var contentNode: (ASDisplayNode & SecureIdAuthContentNode)?
switch verificationState {
case let .passwordChallenge(hint, challengeState, _):
if let current = self.contentNode as? SecureIdAuthPasswordOptionContentNode {
current.updateIsChecking(challengeState == .checking)
if case .invalid = challengeState {
current.updateIsInvalid()
}
contentNode = current
} else {
let current = SecureIdAuthPasswordOptionContentNode(theme: presentationData.theme, strings: presentationData.strings, hint: hint, checkPassword: { [weak self] password in
self?.interaction.checkPassword(password)
}, passwordHelp: { [weak self] in
self?.interaction.openPasswordHelp()
})
current.updateIsChecking(challengeState == .checking)
if case .invalid = challengeState {
current.updateIsInvalid()
}
contentNode = current
}
case .noChallenge:
contentNode = nil
case .verified:
if let _ = list.encryptedValues, let values = list.values {
if let current = self.contentNode as? SecureIdAuthListContentNode {
current.updateValues(values)
contentNode = current
} else {
let current = SecureIdAuthListContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, openField: { [weak self] field in
self?.openListField(field)
}, deleteAll: { [weak self] in
self?.deleteAllValues()
}, requestLayout: { [weak self] in
if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
})
current.updateValues(values)
contentNode = current
}
}
}
if self.contentNode !== contentNode {
self.transitionToContentNode(contentNode, transition: transition)
}
} else {
displayActivity = true
}
}
if displayActivity != !self.activityIndicator.isHidden {
self.activityIndicator.isHidden = !displayActivity
}
}
private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) {
let requestId = self.scheduledLayoutTransitionRequestId
self.scheduledLayoutTransitionRequestId += 1
self.scheduledLayoutTransitionRequest = (requestId, transition)
(self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in
if let strongSelf = self {
if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId {
strongSelf.scheduledLayoutTransitionRequest = nil
strongSelf.requestLayout(currentRequestTransition)
}
}
})
self.setNeedsLayout()
}
private func presentDocumentSelection(field: SecureIdParsedRequestedFormField) {
guard let state = self.state, case let .form(form) = state, let verificationState = form.verificationState, case let .verified(secureIdContext) = verificationState, let encryptedFormData = form.encryptedFormData, let formData = form.formData else {
return
}
let updatedValues: ([SecureIdValueKey], [SecureIdValueWithContext]) -> Void = { [weak self] touchedKeys, updatedValues in
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
guard let formData = form.formData, case let .form(form) = state else {
return state
}
var values = formData.values.filter { value in
return !touchedKeys.contains(value.value.key)
}
values.append(contentsOf: updatedValues)
return .form(SecureIdAuthControllerFormState(twoStepEmail: form.twoStepEmail, encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues))
}
}
switch field {
case let .identity(personalDetails, document):
if let document = document {
var hasValueType: (document: SecureIdRequestedIdentityDocument, requireSelfie: Bool, hasSelfie: Bool, requireTranslation: Bool, hasTranslation: Bool)?
switch document {
case let .just(type):
if let value = findValue(formData.values, key: type.document.valueKey)?.1 {
let data = extractSecureIdValueAdditionalData(value.value)
switch value.value {
case .passport:
hasValueType = (.passport, type.selfie, data.selfie, type.translation, data.translation)
case .idCard:
hasValueType = (.idCard, type.selfie, data.selfie, type.translation, data.translation)
case .driversLicense:
hasValueType = (.driversLicense, type.selfie, data.selfie, type.translation, data.translation)
case .internalPassport:
hasValueType = (.internalPassport, type.selfie, data.selfie, type.translation, data.translation)
default:
break
}
}
case let .oneOf(types):
inner: for type in types.sorted(by: { $0.document.valueKey.rawValue < $1.document.valueKey.rawValue }) {
if let value = findValue(formData.values, key: type.document.valueKey)?.1 {
let data = extractSecureIdValueAdditionalData(value.value)
var dataFilled = true
if type.selfie && !data.selfie {
dataFilled = false
}
if type.translation && !data.translation {
dataFilled = false
}
if hasValueType == nil || dataFilled {
switch value.value {
case .passport:
hasValueType = (.passport, type.selfie, data.selfie, type.translation, data.translation)
case .idCard:
hasValueType = (.idCard, type.selfie, data.selfie, type.translation, data.translation)
case .driversLicense:
hasValueType = (.driversLicense, type.selfie, data.selfie, type.translation, data.translation)
case .internalPassport:
hasValueType = (.internalPassport, type.selfie, data.selfie, type.translation, data.translation)
default:
break
}
if dataFilled {
break inner
}
}
}
}
}
if let (hasValueType, requireSelfie, hasSelfie, requireTranslation, hasTranslation) = hasValueType {
var scrollTo: SecureIdDocumentFormScrollToSubject?
if requireSelfie && !hasSelfie {
scrollTo = .selfie
}
else if requireTranslation && !hasTranslation {
scrollTo = .translation
}
self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .identity(details: personalDetails, document: hasValueType, selfie: requireSelfie, translations: requireTranslation), scrollTo: scrollTo, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in
var keys: [SecureIdValueKey] = []
if personalDetails != nil {
keys.append(.personalDetails)
}
keys.append(hasValueType.valueKey)
updatedValues(keys, values)
}))
return
}
} else if personalDetails != nil {
self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .identity(details: personalDetails, document: nil, selfie: false, translations: false), primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in
updatedValues([.personalDetails], values)
}))
return
}
case let .address(addressDetails, document):
if let document = document {
var hasValueType: (document: SecureIdRequestedAddressDocument, requireTranslation: Bool, hasTranslation: Bool)?
switch document {
case let .just(type):
if let value = findValue(formData.values, key: type.document.valueKey)?.1 {
let data = extractSecureIdValueAdditionalData(value.value)
switch value.value {
case .utilityBill:
hasValueType = (.utilityBill, type.translation, data.translation)
case .bankStatement:
hasValueType = (.bankStatement, type.translation, data.translation)
case .rentalAgreement:
hasValueType = (.rentalAgreement, type.translation, data.translation)
case .passportRegistration:
hasValueType = (.passportRegistration, type.translation, data.translation)
case .temporaryRegistration:
hasValueType = (.temporaryRegistration, type.translation, data.translation)
default:
break
}
}
case let .oneOf(types):
inner: for type in types.sorted(by: { $0.document.valueKey.rawValue < $1.document.valueKey.rawValue }) {
if let value = findValue(formData.values, key: type.document.valueKey)?.1 {
let data = extractSecureIdValueAdditionalData(value.value)
var dataFilled = true
if type.translation && !data.translation {
dataFilled = false
}
if hasValueType == nil || dataFilled {
switch value.value {
case .utilityBill:
hasValueType = (.utilityBill, type.translation, data.translation)
case .bankStatement:
hasValueType = (.bankStatement, type.translation, data.translation)
case .rentalAgreement:
hasValueType = (.rentalAgreement, type.translation, data.translation)
case .passportRegistration:
hasValueType = (.passportRegistration, type.translation, data.translation)
case .temporaryRegistration:
hasValueType = (.temporaryRegistration, type.translation, data.translation)
default:
break
}
if dataFilled {
break inner
}
}
}
}
}
if let (hasValueType, requireTranslation, hasTranslation) = hasValueType {
var scrollTo: SecureIdDocumentFormScrollToSubject?
if requireTranslation && !hasTranslation {
scrollTo = .translation
}
self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .address(details: addressDetails, document: hasValueType, translations: requireTranslation), scrollTo: scrollTo, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in
var keys: [SecureIdValueKey] = []
if addressDetails {
keys.append(.address)
}
keys.append(hasValueType.valueKey)
updatedValues(keys, values)
}))
return
}
} else if addressDetails {
self.interaction.push(SecureIdDocumentFormController(context: self.context, secureIdContext: secureIdContext, requestedData: .address(details: addressDetails, document: nil, translations: false), primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in
updatedValues([.address], values)
}))
return
}
default:
break
}
let completionImpl: (SecureIdDocumentFormRequestedData) -> Void = { [weak self] requestedData in
guard let strongSelf = self, let state = strongSelf.state, let verificationState = state.verificationState, case .verified = verificationState, let formData = form.formData, let validLayout = strongSelf.validLayout?.0 else {
return
}
var attachmentType: SecureIdAttachmentMenuType? = nil
var attachmentTarget: SecureIdAddFileTarget? = nil
switch requestedData {
case let .identity(_, document, _, _):
if let document = document {
switch document {
case .idCard, .driversLicense:
attachmentType = .idCard
default:
attachmentType = .generic
}
attachmentTarget = .frontSide(document)
}
case .address:
attachmentType = .multiple
attachmentTarget = .scan
}
let controller = SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: requestedData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, values: formData.values, updatedValues: { values in
var keys: [SecureIdValueKey] = []
switch requestedData {
case let .identity(details, document, _, _):
if details != nil {
keys.append(.personalDetails)
}
if let document = document {
keys.append(document.valueKey)
}
case let .address(details, document, _):
if details {
keys.append(.address)
}
if let document = document {
keys.append(document.valueKey)
}
}
updatedValues(keys, values)
})
if let attachmentType = attachmentType, let type = attachmentTarget {
presentLegacySecureIdAttachmentMenu(context: strongSelf.context, present: { [weak self] c in
self?.interaction.present(c, nil)
}, validLayout: validLayout, type: attachmentType, recognizeDocumentData: true, completion: { [weak self] resources, recognizedData in
guard let strongSelf = self else {
return
}
strongSelf.interaction.present(controller, nil)
controller.addDocuments(type: type, resources: resources, recognizedData: recognizedData, removeDocumentId: nil)
})
} else {
strongSelf.interaction.present(controller, nil)
}
}
let itemsForField = documentSelectionItemsForField(field: field, strings: self.presentationData.strings)
if itemsForField.count == 1 {
completionImpl(itemsForField[0].1)
} else {
let controller = SecureIdDocumentTypeSelectionController(context: self.context, field: field, currentValues: formData.values, completion: completionImpl)
self.interaction.present(controller, nil)
}
}
private func presentPlaintextSelection(type: SecureIdPlaintextFormType) {
guard let state = self.state, case let .form(form) = state, let formData = form.formData, let verificationState = form.verificationState, case let .verified(secureIdContext) = verificationState else {
return
}
var immediatelyAvailableValue: SecureIdValue?
var currentValue: SecureIdValueWithContext?
switch type {
case .phone:
if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
}
currentValue = findValue(formData.values, key: .phone)?.1
case .email:
if let email = form.twoStepEmail {
immediatelyAvailableValue = .email(SecureIdEmailValue(email: email))
}
currentValue = findValue(formData.values, key: .email)?.1
}
let openForm: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.interaction.push(SecureIdPlaintextFormController(context: strongSelf.context, secureIdContext: secureIdContext, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { valueWithContext in
if let strongSelf = self {
strongSelf.interaction.updateState { state in
if case let .form(form) = state, let formData = form.formData {
var values = formData.values
switch type {
case .phone:
while let index = findValue(values, key: .phone)?.0 {
values.remove(at: index)
}
case .email:
while let index = findValue(values, key: .email)?.0 {
values.remove(at: index)
}
}
if let valueWithContext = valueWithContext {
values.append(valueWithContext)
}
return .form(SecureIdAuthControllerFormState(twoStepEmail: form.twoStepEmail, encryptedFormData: form.encryptedFormData, formData: SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: values), verificationState: form.verificationState, removingValues: form.removingValues))
}
return state
}
}
}))
}
if let currentValue = currentValue {
let controller = ActionSheetController(presentationData: self.presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let text: String
switch currentValue.value {
case .phone:
text = self.presentationData.strings.Passport_Phone_Delete
default:
text = self.presentationData.strings.Passport_Email_Delete
}
controller.setItemGroups([
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .form(form) = state {
form.removingValues = true
return .form(form)
}
return state
}
strongSelf.deleteValueDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set([currentValue.value.key]))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .form(form) = state, let formData = form.formData {
form.removingValues = false
form.formData = SecureIdForm(peerId: formData.peerId, requestedFields: formData.requestedFields, values: formData.values.filter {
$0.value.key != currentValue.value.key
})
return .form(form)
}
return state
}
}))
})]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.endEditing(true)
self.interaction.present(controller, nil)
} else {
openForm()
}
}
private func openListField(_ field: SecureIdAuthListContentField) {
guard let state = self.state, case let .list(list) = state, let verificationState = list.verificationState, case let .verified(secureIdContext) = verificationState else {
return
}
guard let values = list.values else {
return
}
let updatedValues: (SecureIdValueKey) -> ([SecureIdValueWithContext]) -> Void = { valueKey in
return { [weak self] updatedValues in
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
guard case var .list(list) = state, var values = list.values else {
return state
}
values = values.filter({ value in
return value.value.key != valueKey
})
values.append(contentsOf: updatedValues)
list.values = values
return .list(list)
}
}
}
let openAction: (SecureIdValueKey) -> Void = { [weak self] field in
guard let strongSelf = self, let state = strongSelf.state, case let .list(list) = state else {
return
}
let primaryLanguageByCountry = list.primaryLanguageByCountry ?? [:]
switch field {
case .personalDetails:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: ParsedRequestedPersonalDetails(nativeNames: false), document: nil, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .passport:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .passport, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .internalPassport:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .internalPassport, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .driversLicense:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .driversLicense, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .idCard:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .identity(details: nil, document: .idCard, selfie: false, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .address:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: true, document: nil, translations: false), primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .utilityBill:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .utilityBill, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .bankStatement:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .bankStatement, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .rentalAgreement:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .rentalAgreement, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .passportRegistration:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .passportRegistration, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .temporaryRegistration:
strongSelf.interaction.push(SecureIdDocumentFormController(context: strongSelf.context, secureIdContext: secureIdContext, requestedData: .address(details: false, document: .temporaryRegistration, translations: false), requestOptionalData: true, primaryLanguageByCountry: primaryLanguageByCountry, values: values, updatedValues: updatedValues(field)))
case .phone:
break
case .email:
break
}
}
let deleteField: (SecureIdValueKey) -> Void = { [weak self] field in
guard let strongSelf = self else {
return
}
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let text: String
switch field {
case .phone:
text = strongSelf.presentationData.strings.Passport_Phone_Delete
default:
text = strongSelf.presentationData.strings.Passport_Email_Delete
}
controller.setItemGroups([
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .list(list) = state {
list.removingValues = true
return .list(list)
}
return state
}
strongSelf.deleteValueDisposable.set((deleteSecureIdValues(network: strongSelf.context.account.network, keys: Set([field]))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
strongSelf.interaction.updateState { state in
if case var .list(list) = state , let values = list.values {
list.removingValues = false
list.values = values.filter {
$0.value.key != field
}
return .list(list)
}
return state
}
}))
})]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
strongSelf.view.endEditing(true)
strongSelf.interaction.present(controller, nil)
}
switch field {
case .identity, .address:
let keys: [(SecureIdValueKey, String, String)]
let strings = self.presentationData.strings
if case .identity = field {
keys = [
(.personalDetails, strings.Passport_Identity_AddPersonalDetails, strings.Passport_Identity_EditPersonalDetails),
(.passport, strings.Passport_Identity_AddPassport, strings.Passport_Identity_EditPassport),
(.idCard, strings.Passport_Identity_AddIdentityCard, strings.Passport_Identity_EditIdentityCard),
(.driversLicense, strings.Passport_Identity_AddDriversLicense, strings.Passport_Identity_EditDriversLicense),
(.internalPassport, strings.Passport_Identity_AddInternalPassport, strings.Passport_Identity_EditInternalPassport),
]
} else {
keys = [
(.address, strings.Passport_Address_AddResidentialAddress, strings.Passport_Address_EditResidentialAddress), (.utilityBill, strings.Passport_Address_AddUtilityBill, strings.Passport_Address_EditUtilityBill),
(.bankStatement, strings.Passport_Address_AddBankStatement, strings.Passport_Address_EditBankStatement),
(.rentalAgreement, strings.Passport_Address_AddRentalAgreement, strings.Passport_Address_EditRentalAgreement),
(.passportRegistration, strings.Passport_Address_AddPassportRegistration, strings.Passport_Address_EditPassportRegistration),
(.temporaryRegistration, strings.Passport_Address_AddTemporaryRegistration, strings.Passport_Address_EditTemporaryRegistration)
]
}
let controller = ActionSheetController(presentationData: self.presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var items: [ActionSheetItem] = []
for (key, add, edit) in keys {
items.append(ActionSheetButtonItem(title: findValue(values, key: key) != nil ? edit : add, action: {
dismissAction()
openAction(key)
}))
}
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.endEditing(true)
self.interaction.present(controller, nil)
case .phone:
if findValue(values, key: .phone) != nil {
deleteField(.phone)
} else {
var immediatelyAvailableValue: SecureIdValue?
if let peer = list.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
}
self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in
updatedValues(.phone)(value.flatMap({ [$0] }) ?? [])
}))
}
case .email:
if findValue(values, key: .email) != nil {
deleteField(.email)
} else {
var immediatelyAvailableValue: SecureIdValue?
if let email = list.twoStepEmail {
immediatelyAvailableValue = .email(SecureIdEmailValue(email: email))
}
self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .email, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in
updatedValues(.email)(value.flatMap({ [$0] }) ?? [])
}))
}
}
}
private func deleteAllValues() {
let controller = ActionSheetController(presentationData: self.presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let items: [ActionSheetItem] = [
ActionSheetTextItem(title: self.presentationData.strings.Passport_DeletePassportConfirmation),
ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, enabled: true, action: { [weak self] in
dismissAction()
self?.interaction.deleteAll()
})
]
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.endEditing(true)
self.interaction.present(controller, nil)
}
}
@@ -0,0 +1,155 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
struct SecureIdEncryptedFormData {
let form: EncryptedSecureIdForm
let primaryLanguageByCountry: [String: String]
let accountPeer: Peer
let servicePeer: Peer
}
enum SecureIdAuthPasswordChallengeState {
case none
case checking
case invalid
}
enum SecureIdAuthControllerVerificationNoChallengeState: Equatable {
case notSet
case awaitingConfirmation(password: String?, emailPattern: String, codeLength: Int32?)
}
enum SecureIdAuthControllerVerificationState: Equatable {
case noChallenge(SecureIdAuthControllerVerificationNoChallengeState)
case passwordChallenge(hint: String, state: SecureIdAuthPasswordChallengeState, hasRecoveryEmail: Bool)
case verified(SecureIdAccessContext)
}
struct SecureIdAuthControllerFormState: Equatable {
var twoStepEmail: String?
var encryptedFormData: SecureIdEncryptedFormData?
var formData: SecureIdForm?
var verificationState: SecureIdAuthControllerVerificationState?
var removingValues: Bool = false
static func ==(lhs: SecureIdAuthControllerFormState, rhs: SecureIdAuthControllerFormState) -> Bool {
if let lhsTwoStepEmail = lhs.twoStepEmail, let rhsTwoStepEmail = rhs.twoStepEmail, lhsTwoStepEmail != rhsTwoStepEmail {
return false
} else if (lhs.twoStepEmail != nil) != (rhs.twoStepEmail != nil) {
return false
}
if (lhs.encryptedFormData != nil) != (rhs.encryptedFormData != nil) {
return false
}
if (lhs.formData != nil) != (rhs.formData != nil) {
return false
}
if let lhsFormData = lhs.formData, let rhsFormData = rhs.formData {
if lhsFormData != rhsFormData {
return false
}
} else if (lhs.formData != nil) != (rhs.formData != nil) {
return false
}
if lhs.verificationState != rhs.verificationState {
return false
}
if lhs.removingValues != rhs.removingValues {
return false
}
return true
}
}
struct SecureIdAuthControllerListState: Equatable {
var accountPeer: Peer?
var twoStepEmail: String?
var verificationState: SecureIdAuthControllerVerificationState?
var encryptedValues: EncryptedAllSecureIdValues?
var primaryLanguageByCountry: [String: String]?
var values: [SecureIdValueWithContext]?
var removingValues: Bool = false
static func ==(lhs: SecureIdAuthControllerListState, rhs: SecureIdAuthControllerListState) -> Bool {
if !arePeersEqual(lhs.accountPeer, rhs.accountPeer) {
return false
}
if let lhsTwoStepEmail = lhs.twoStepEmail, let rhsTwoStepEmail = rhs.twoStepEmail, lhsTwoStepEmail != rhsTwoStepEmail {
return false
} else if (lhs.twoStepEmail != nil) != (rhs.twoStepEmail != nil) {
return false
}
if lhs.verificationState != rhs.verificationState {
return false
}
if (lhs.encryptedValues != nil) != (rhs.encryptedValues != nil) {
return false
}
if lhs.primaryLanguageByCountry != rhs.primaryLanguageByCountry {
return false
}
if lhs.values != rhs.values {
return false
}
if lhs.removingValues != rhs.removingValues {
return false
}
return true
}
}
enum SecureIdAuthControllerState: Equatable {
case form(SecureIdAuthControllerFormState)
case list(SecureIdAuthControllerListState)
var twoStepEmail: String? {
get {
switch self {
case let .form(form):
return form.twoStepEmail
case let .list(list):
return list.twoStepEmail
}
} set(value) {
switch self {
case var .form(form):
form.twoStepEmail = value
self = .form(form)
case var .list(list):
list.twoStepEmail = value
self = .list(list)
}
}
}
var verificationState: SecureIdAuthControllerVerificationState? {
get {
switch self {
case let .form(form):
return form.verificationState
case let .list(list):
return list.verificationState
}
} set(value) {
switch self {
case var .form(form):
form.verificationState = value
self = .form(form)
case var .list(list):
list.verificationState = value
self = .list(list)
}
}
}
var removingValues: Bool {
switch self {
case let .form(form):
return form.removingValues
case let .list(list):
return list.removingValues
}
}
}
@@ -0,0 +1,171 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import Markdown
private let infoFont = Font.regular(14.0)
private let passwordFont = Font.regular(16.0)
private let buttonFont = Font.regular(17.0)
final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate {
private let primaryLanguageByCountry: [String: String]
private let requestedFields: [SecureIdRequestedFormField]
private let fieldBackgroundNode: ASDisplayNode
private let fieldNodes: [SecureIdAuthFormFieldNode]
private let headerNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let requestLayout: () -> Void
private var validLayout: CGFloat?
init(theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peer: EnginePeer, privacyPolicyUrl: String?, form: SecureIdForm, primaryLanguageByCountry: [String: String], openField: @escaping (SecureIdParsedRequestedFormField) -> Void, openURL: @escaping (String) -> Void, openMention: @escaping (TelegramPeerMention) -> Void, requestLayout: @escaping () -> Void) {
self.requestLayout = requestLayout
self.primaryLanguageByCountry = primaryLanguageByCountry
self.requestedFields = form.requestedFields
self.fieldBackgroundNode = ASDisplayNode()
self.fieldBackgroundNode.isLayerBacked = true
self.fieldBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
var fieldNodes: [SecureIdAuthFormFieldNode] = []
for (field, fieldValues, _) in parseRequestedFormFields(self.requestedFields, values: form.values, primaryLanguageByCountry: primaryLanguageByCountry) {
fieldNodes.append(SecureIdAuthFormFieldNode(theme: theme, strings: strings, field: field, values: fieldValues, primaryLanguageByCountry: primaryLanguageByCountry, selected: {
openField(field)
}))
}
self.fieldNodes = fieldNodes
self.headerNode = ImmediateTextNode()
self.headerNode.displaysAsynchronously = false
self.headerNode.attributedText = NSAttributedString(string: strings.Passport_RequestedInformation, font: infoFont, textColor: theme.list.sectionHeaderTextColor)
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.2
let text: NSAttributedString
if let privacyPolicyUrl = privacyPolicyUrl {
let privacyPolicyAttributes = MarkdownAttributeSet(font: infoFont, textColor: theme.list.freeTextColor)
let privacyPolicyLinkAttributes = MarkdownAttributeSet(font: infoFont, textColor: theme.list.itemAccentColor, additionalAttributes: [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber, TelegramTextAttributes.URL: privacyPolicyUrl])
text = parseMarkdownIntoAttributedString(strings.Passport_PrivacyPolicy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder), (peer.addressName ?? "")).string.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: privacyPolicyAttributes, bold: privacyPolicyAttributes, link: privacyPolicyLinkAttributes, linkAttribute: { _ in
return nil
}), textAlignment: .center)
} else {
text = NSAttributedString(string: strings.Passport_AcceptHelp(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder), (peer.addressName ?? "")).string, font: infoFont, textColor: theme.list.freeTextColor, paragraphAlignment: .left)
}
self.textNode.attributedText = text
super.init()
self.textNode.linkHighlightColor = theme.list.itemAccentColor.withAlphaComponent(0.2)
self.textNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)
} else {
return nil
}
}
self.textNode.tapAttributeAction = { attributes, _ in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
openURL(url)
} else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
openMention(mention)
}
}
self.addSubnode(self.headerNode)
self.addSubnode(self.fieldBackgroundNode)
self.addSubnode(self.textNode)
self.fieldNodes.forEach(self.addSubnode)
}
func updateValues(_ values: [SecureIdValueWithContext]) {
var index = 0
for (_, fieldValues, _) in parseRequestedFormFields(self.requestedFields, values: values, primaryLanguageByCountry: self.primaryLanguageByCountry) {
if index < self.fieldNodes.count {
self.fieldNodes[index].updateValues(fieldValues, primaryLanguageByCountry: self.primaryLanguageByCountry)
}
index += 1
}
self.requestLayout()
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout {
let transition = self.validLayout == nil ? .immediate : transition
self.validLayout = width
var contentHeight: CGFloat = 0.0
let headerSpacing: CGFloat = 6.0
let headerSize = self.headerNode.updateLayout(CGSize(width: width - 14.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: headerSize))
contentHeight += headerSize.height + headerSpacing
let fieldsOrigin = contentHeight
for i in 0 ..< self.fieldNodes.count {
let fieldHeight = self.fieldNodes[i].updateLayout(width: width, hasPrevious: i != 0, hasNext: i != self.fieldNodes.count - 1, transition: transition)
transition.updateFrame(node: self.fieldNodes[i], frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: fieldHeight)))
contentHeight += fieldHeight
}
let fieldsHeight = contentHeight - fieldsOrigin
let textSpacing: CGFloat = 6.0
contentHeight += textSpacing
let textSize = self.textNode.updateLayout(CGSize(width: width - 14.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: 14.0, y: contentHeight), size: textSize))
contentHeight += textSize.height
transition.updateFrame(node: self.fieldBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: fieldsOrigin), size: CGSize(width: width, height: fieldsHeight)))
return SecureIdAuthContentLayout(height: contentHeight, centerOffset: floor((contentHeight) / 2.0) - 34.0)
}
func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
}
func didAppear() {
}
func willDisappear() {
}
func frameForField(_ field: SecureIdParsedRequestedFormField) -> CGRect? {
for fieldNode in self.fieldNodes {
if fieldNode.field == field {
return fieldNode.frame
}
}
return nil
}
func highlightField(_ field: SecureIdParsedRequestedFormField) {
for fieldNode in self.fieldNodes {
if fieldNode.field == field {
fieldNode.highlight()
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import AvatarNode
import AppBundle
import AccountContext
private let avatarFont = avatarPlaceholderFont(size: 26.0)
private let titleFont = Font.semibold(14.0)
private let textFont = Font.regular(14.0)
final class SecureIdAuthHeaderNode: ASDisplayNode {
private let context: AccountContext
private let theme: PresentationTheme
private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder
private let serviceAvatarNode: AvatarNode
private let titleNode: ImmediateTextNode
private let iconNode: ASImageNode
private var verificationState: SecureIdAuthControllerVerificationState?
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
self.context = context
self.theme = theme
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.serviceAvatarNode = AvatarNode(font: avatarFont)
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/ViewPassportIcon"), color: theme.list.freeMonoIconColor)
super.init()
self.addSubnode(self.serviceAvatarNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.iconNode)
}
func updateState(formData: SecureIdEncryptedFormData?, verificationState: SecureIdAuthControllerVerificationState) {
if let formData = formData {
self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: EnginePeer(formData.servicePeer))
let titleData = self.strings.Passport_RequestHeader(EnginePeer(formData.servicePeer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder))
let titleString = NSMutableAttributedString()
titleString.append(NSAttributedString(string: titleData.string, font: textFont, textColor: self.theme.list.freeTextColor))
for range in titleData.ranges {
titleString.addAttribute(.font, value: titleFont, range: range.range)
}
self.titleNode.attributedText = titleString
self.iconNode.isHidden = true
} else {
self.iconNode.isHidden = false
self.titleNode.isHidden = true
self.serviceAvatarNode.isHidden = true
}
self.verificationState = verificationState
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> (compact: CGFloat, expanded: CGFloat, apply: (Bool) -> Void) {
if !self.iconNode.isHidden {
guard let image = self.iconNode.image else {
return (1.0, 1.0, { _ in
})
}
return (image.size.height, image.size.height, { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0), y: 0.0), size: image.size)
})
} else {
let avatarSize = CGSize(width: 70.0, height: 70.0)
let avatarTitleSpacing: CGFloat = 20.0
let titleSize = self.titleNode.updateLayout(CGSize(width: width - 20.0, height: 1000.0))
if let verificationState = self.verificationState, case .noChallenge = verificationState {
self.serviceAvatarNode.isHidden = true
} else {
self.serviceAvatarNode.isHidden = false
}
var expandedHeight: CGFloat = titleSize.height
if !self.serviceAvatarNode.isHidden {
expandedHeight += avatarSize.height + avatarTitleSpacing
}
let compactHeight = titleSize.height
return (compactHeight, expandedHeight, { [weak self] expanded in
guard let strongSelf = self else {
return
}
transition.updateAlpha(node: strongSelf.serviceAvatarNode, alpha: expanded ? 1.0 : 0.0)
var titleOffset: CGFloat = 0.0
if expanded && !strongSelf.serviceAvatarNode.isHidden && !strongSelf.serviceAvatarNode.alpha.isZero {
titleOffset = avatarSize.height + avatarTitleSpacing
}
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: titleOffset), size: titleSize)
let previousTitleFrame = strongSelf.titleNode.frame
ContainedViewLayoutTransition.immediate.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
transition.animatePositionAdditive(node: strongSelf.titleNode, offset: CGPoint(x: 0.0, y: previousTitleFrame.midY - titleFrame.midY))
let serviceAvatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize.width) / 2.0), y: titleFrame.minY - avatarTitleSpacing - avatarSize.height), size: avatarSize)
transition.updateFrame(node: strongSelf.serviceAvatarNode, frame: serviceAvatarFrame)
})
}
}
}
@@ -0,0 +1,129 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
final class SecureIdAuthListContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let dateTimeFormat: PresentationDateTimeFormat
private let fieldBackgroundNode: ASDisplayNode
private let fieldNodes: [SecureIdAuthListFieldNode]
private let headerNode: ImmediateTextNode
private let deleteItem: FormControllerActionItem
private let deleteNode: FormControllerActionItemNode
private let requestLayout: () -> Void
private var validLayout: CGFloat?
init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, openField: @escaping (SecureIdAuthListContentField) -> Void, deleteAll: @escaping () -> Void, requestLayout: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.fieldBackgroundNode = ASDisplayNode()
self.fieldBackgroundNode.isLayerBacked = true
self.fieldBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
var fieldNodes: [SecureIdAuthListFieldNode] = []
fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .identity, values: [], selected: {
openField(.identity)
}))
fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .address, values: [], selected: {
openField(.address)
}))
fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .phone, values: [], selected: {
openField(.phone)
}))
fieldNodes.append(SecureIdAuthListFieldNode(theme: theme, strings: strings, field: .email, values: [], selected: {
openField(.email)
}))
self.fieldNodes = fieldNodes
self.headerNode = ImmediateTextNode()
self.headerNode.displaysAsynchronously = false
self.headerNode.attributedText = NSAttributedString(string: strings.Passport_PassportInformation, font: Font.regular(14.0), textColor: theme.list.sectionHeaderTextColor)
self.deleteItem = FormControllerActionItem(type: .destructive, title: strings.Passport_DeletePassport, activated: {
deleteAll()
})
self.deleteNode = self.deleteItem.node() as! FormControllerActionItemNode
self.requestLayout = requestLayout
super.init()
self.addSubnode(self.headerNode)
self.addSubnode(self.fieldBackgroundNode)
self.addSubnode(self.deleteNode)
self.fieldNodes.forEach(self.addSubnode)
}
func updateValues(_ values: [SecureIdValueWithContext]) {
for fieldNode in self.fieldNodes {
fieldNode.updateValues(values)
}
self.deleteNode.isHidden = values.isEmpty
self.requestLayout()
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout {
let transition = self.validLayout == nil ? .immediate : transition
self.validLayout = width
var contentHeight: CGFloat = 0.0
let headerSpacing: CGFloat = 6.0
let headerSize = self.headerNode.updateLayout(CGSize(width: width - 14.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: headerSize))
contentHeight += headerSize.height + headerSpacing
let fieldsOrigin = contentHeight
for i in 0 ..< self.fieldNodes.count {
let fieldHeight = self.fieldNodes[i].updateLayout(width: width, hasPrevious: i != 0, hasNext: i != self.fieldNodes.count - 1, transition: transition)
transition.updateFrame(node: self.fieldNodes[i], frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: fieldHeight)))
contentHeight += fieldHeight
}
let fieldsHeight = contentHeight - fieldsOrigin
transition.updateFrame(node: self.fieldBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: fieldsOrigin), size: CGSize(width: width, height: fieldsHeight)))
let deleteSpacing: CGFloat = 32.0
contentHeight += deleteSpacing
let (preLayout, apply) = self.deleteItem.update(node: self.deleteNode, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, width: width, previousNeighbor: .spacer, nextNeighbor: .spacer, transition: transition)
let deleteHeight = apply(FormControllerItemLayoutParams(maxAligningInset: preLayout.aligningInset))
transition.updateFrame(node: self.deleteNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: deleteHeight)))
contentHeight += deleteHeight
contentHeight += deleteSpacing
return SecureIdAuthContentLayout(height: contentHeight, centerOffset: floor((contentHeight) / 2.0))
}
func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
}
func didAppear() {
}
func willDisappear() {
}
}
@@ -0,0 +1,253 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import PhoneNumberFormat
private let titleFont = Font.regular(17.0)
private let textFont = Font.regular(15.0)
private func fieldsText(_ fields: String...) -> String {
var result = ""
for field in fields {
if !field.isEmpty {
if !result.isEmpty {
result.append(", ")
}
result.append(field)
}
}
return result
}
private func fieldsText(_ fields: [String]) -> String {
var result = ""
for field in fields {
if !field.isEmpty {
if !result.isEmpty {
result.append(", ")
}
result.append(field)
}
}
return result
}
private func fieldTitleAndText(field: SecureIdAuthListContentField, strings: PresentationStrings, values: [SecureIdValueWithContext]) -> (String, String) {
let title: String
let placeholder: String
var text: String = ""
switch field {
case .identity:
title = strings.Passport_FieldIdentity
placeholder = strings.Passport_FieldIdentityDetailsHelp
let keyList: [(SecureIdValueKey, String)] = [
(.personalDetails, strings.Passport_Identity_TypePersonalDetails),
(.passport, strings.Passport_Identity_TypePassport),
(.idCard, strings.Passport_Identity_TypeIdentityCard),
(.driversLicense, strings.Passport_Identity_TypeDriversLicense),
(.internalPassport, strings.Passport_Identity_TypeInternalPassport)
]
var fields: [String] = []
for (key, valueTitle) in keyList {
if findValue(values, key: key) != nil {
fields.append(valueTitle)
}
}
if !fields.isEmpty {
text = fieldsText(fields)
}
case .address:
title = strings.Passport_FieldAddress
placeholder = strings.Passport_FieldAddressHelp
let keyList: [(SecureIdValueKey, String)] = [
(.address, strings.Passport_Address_TypeResidentialAddress),
(.utilityBill, strings.Passport_Address_TypeUtilityBill),
(.bankStatement, strings.Passport_Address_TypeBankStatement),
(.rentalAgreement, strings.Passport_Address_TypeRentalAgreement),
(.passportRegistration, strings.Passport_Address_TypePassportRegistration),
(.temporaryRegistration, strings.Passport_Address_TypeTemporaryRegistration)
]
var fields: [String] = []
for (key, valueTitle) in keyList {
if findValue(values, key: key) != nil {
fields.append(valueTitle)
}
}
if !fields.isEmpty {
text = fieldsText(fields)
}
case .phone:
title = strings.Passport_FieldPhone
placeholder = strings.Passport_FieldPhoneHelp
if let value = findValue(values, key: .phone), case let .phone(phoneValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
text = formatPhoneNumber(phoneValue.phone)
}
case .email:
title = strings.Passport_FieldEmail
placeholder = strings.Passport_FieldEmailHelp
if let value = findValue(values, key: .email), case let .email(emailValue) = value.1.value {
if !text.isEmpty {
text.append(", ")
}
text = formatPhoneNumber(emailValue.email)
}
}
return (title, text.isEmpty ? placeholder : text)
}
enum SecureIdAuthListContentField {
case identity
case address
case phone
case email
}
final class SecureIdAuthListFieldNode: ASDisplayNode {
private let selected: () -> Void
private let topSeparatorNode: ASDisplayNode
private let bottomSeparatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let disclosureNode: ASImageNode
private let buttonNode: HighlightableButtonNode
private var validLayout: (CGFloat, Bool, Bool)?
private let field: SecureIdAuthListContentField
private let theme: PresentationTheme
private let strings: PresentationStrings
init(theme: PresentationTheme, strings: PresentationStrings, field: SecureIdAuthListContentField, values: [SecureIdValueWithContext], selected: @escaping () -> Void) {
self.field = field
self.theme = theme
self.strings = strings
self.selected = selected
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
self.highlightedBackgroundNode.alpha = 0.0
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.isUserInteractionEnabled = false
self.titleNode.maximumNumberOfLines = 1
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.textNode.maximumNumberOfLines = 4
self.disclosureNode = ASImageNode()
self.disclosureNode.isLayerBacked = true
self.disclosureNode.displayWithoutProcessing = true
self.disclosureNode.displaysAsynchronously = false
self.disclosureNode.image = PresentationResourcesItemList.disclosureArrowImage(theme)
self.buttonNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.disclosureNode)
self.addSubnode(self.buttonNode)
self.updateValues(values)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
strongSelf.view.superview?.bringSubviewToFront(strongSelf.view)
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
func updateValues(_ values: [SecureIdValueWithContext]) {
let (title, text) = fieldTitleAndText(field: self.field, strings: self.strings, values: values)
let textColor = self.theme.list.itemSecondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: textColor)
self.disclosureNode.isHidden = false
if let (width, hasPrevious, hasNext) = self.validLayout {
let _ = self.updateLayout(width: width, hasPrevious: hasPrevious, hasNext: hasNext, transition: .immediate)
}
}
func updateLayout(width: CGFloat, hasPrevious: Bool, hasNext: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = (width, hasPrevious, hasNext)
let leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0
let rightTextInset = rightInset + 24.0
let titleTextSpacing: CGFloat = 5.0
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - rightTextInset, height: 100.0))
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightTextInset, height: 100.0))
let height = max(64.0, 11.0 + titleSize.height + titleTextSpacing + textSize.height + 11.0)
let textOrigin = floor((height - titleSize.height - titleTextSpacing - textSize.height) / 2.0)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: textOrigin), size: titleSize)
self.titleNode.frame = titleFrame
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleTextSpacing), size: textSize)
self.textNode.frame = textFrame
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
transition.updateAlpha(node: self.topSeparatorNode, alpha: hasPrevious ? 0.0 : 1.0)
let bottomSeparatorInset: CGFloat = hasNext ? leftInset : 0.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: bottomSeparatorInset, y: height - UIScreenPixel), size: CGSize(width: width - bottomSeparatorInset, height: UIScreenPixel)))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -(hasPrevious ? UIScreenPixel : 0.0)), size: CGSize(width: width, height: height + (hasPrevious ? UIScreenPixel : 0.0))))
if let image = self.disclosureNode.image {
self.disclosureNode.frame = CGRect(origin: CGPoint(x: width - 7.0 - image.size.width, y: floor((height - image.size.height) / 2.0)), size: image.size)
}
return height
}
@objc private func buttonPressed() {
self.selected()
}
}
@@ -0,0 +1,220 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ActivityIndicator
import AppBundle
private let passwordFont = Font.regular(16.0)
private let buttonFont = Font.regular(17.0)
final class SecureIdAuthPasswordOptionContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate {
private let checkPassword: (String) -> Void
private let passwordHelp: () -> Void
private let inputContainer: ASDisplayNode
private let titleNode: ImmediateTextNode
private let inputBackground: ASImageNode
private let inputField: TextFieldNode
private var clearOnce: Bool = false
private let inputButtonNode: HighlightableButtonNode
private let inputActivityNode: ActivityIndicator
private let buttonNode: HighlightableButtonNode
private let buttonBackground: ASImageNode
private let buttonLabel: ImmediateTextNode
private var validLayout: CGFloat?
private var isChecking = false
private let hapticFeedback = HapticFeedback()
init(theme: PresentationTheme, strings: PresentationStrings, hint: String, checkPassword: @escaping (String) -> Void, passwordHelp: @escaping () -> Void) {
self.checkPassword = checkPassword
self.passwordHelp = passwordHelp
self.inputContainer = ASDisplayNode()
self.inputBackground = ASImageNode()
self.inputBackground.isLayerBacked = true
self.inputBackground.displaysAsynchronously = false
self.inputBackground.displayWithoutProcessing = true
self.titleNode = ImmediateTextNode()
self.titleNode.attributedText = NSAttributedString(string: strings.Passport_PasswordHelp, font: Font.regular(14.0), textColor: theme.list.freeTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.inputField = TextFieldNode()
self.inputButtonNode = HighlightableButtonNode()
self.inputActivityNode = ActivityIndicator(type: .custom(theme.list.freeInputField.controlColor, 18.0, 1.5, false))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/PasswordHelpIcon"), color: theme.list.freeInputField.controlColor) {
self.inputButtonNode.setImage(image, for: [])
self.inputButtonNode.frame = CGRect(origin: CGPoint(), size: image.size)
}
self.inputBackground.image = generateStretchableFilledCircleImage(radius: 10.0, color: theme.list.freeInputField.backgroundColor)
self.inputField.textField.isSecureTextEntry = true
self.inputField.textField.font = passwordFont
self.inputField.textField.textColor = theme.list.freeInputField.primaryColor
self.inputField.textField.attributedPlaceholder = NSAttributedString(string: hint.isEmpty ? strings.LoginPassword_PasswordPlaceholder : hint, font: passwordFont, textColor: theme.list.freeInputField.placeholderColor)
self.inputField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.inputField.textField.tintColor = theme.list.itemAccentColor
self.buttonNode = HighlightableButtonNode()
self.buttonBackground = ASImageNode()
self.buttonBackground.isLayerBacked = true
self.buttonBackground.displaysAsynchronously = false
self.buttonBackground.displayWithoutProcessing = true
self.buttonBackground.image = generateStretchableFilledCircleImage(radius: 10.0, color: theme.list.itemCheckColors.fillColor)
self.buttonNode.addSubnode(self.buttonBackground)
self.buttonLabel = ImmediateTextNode()
self.buttonLabel.attributedText = NSAttributedString(string: strings.Common_Next, font: buttonFont, textColor: theme.list.itemCheckColors.foregroundColor)
self.buttonNode.addSubnode(self.buttonLabel)
super.init()
self.inputContainer.addSubnode(self.titleNode)
self.inputContainer.addSubnode(self.inputBackground)
self.inputContainer.addSubnode(self.inputField)
self.inputContainer.addSubnode(self.inputButtonNode)
self.inputContainer.addSubnode(self.inputActivityNode)
self.inputContainer.addSubnode(self.buttonNode)
self.addSubnode(self.inputContainer)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonBackground.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackground.alpha = 0.55
} else {
strongSelf.buttonBackground.alpha = 1.0
strongSelf.buttonBackground.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.inputField.textField.delegate = self
self.inputButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -4.0)
self.inputButtonNode.addTarget(self, action: #selector(self.inputButtonPressed), forControlEvents: .touchUpInside)
self.inputActivityNode.isHidden = true
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout {
let transition = self.validLayout == nil ? .immediate : transition
self.validLayout = width
let inputWidth = min(270.0, width - 30.0)
let labelSize = self.buttonLabel.updateLayout(CGSize(width: width - 20.0, height: 100.0))
let buttonSize = CGSize(width: max(labelSize.width + 30.0, 100.0), height: 36.0)
let titleSpacing: CGFloat = 15.0
let titleSize = self.titleNode.updateLayout(CGSize(width: inputWidth, height: CGFloat.greatestFiniteMagnitude))
let buttonSpacing: CGFloat = 16.0
let inputFrame = CGRect(origin: CGPoint(x: floor((width - inputWidth) / 2.0), y: titleSize.height + titleSpacing), size: CGSize(width: inputWidth, height: 32.0))
let inputContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: titleSize.height + titleSpacing + inputFrame.height + buttonSpacing + buttonSize.height))
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: 0.0), size: titleSize)
transition.updateFrame(node: self.inputContainer, frame: inputContainerFrame)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
transition.updateFrame(node: self.inputBackground, frame: inputFrame)
var inputFieldFrame = inputFrame.insetBy(dx: 6.0, dy: 0.0)
inputFieldFrame.size.width -= 16.0
transition.updateFrame(node: self.inputField, frame: inputFieldFrame)
transition.updateFrame(node: self.inputButtonNode, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - self.inputButtonNode.bounds.size.width - 6.0, y: inputFrame.minY + floor((inputFrame.height - self.inputButtonNode.bounds.size.height) / 2.0)), size: self.inputButtonNode.bounds.size))
let activitySize = CGSize(width: 18.0, height: 18.0)
transition.updateFrame(node: self.inputActivityNode, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 6.0, y: inputFrame.minY + floor((inputFrame.height - activitySize.height) / 2.0)), size: activitySize))
let buttonBounds = CGRect(origin: CGPoint(), size: buttonSize)
transition.updateFrame(node: self.buttonNode, frame: buttonBounds.offsetBy(dx: floor((width - buttonSize.width) / 2.0), dy: inputFrame.maxY + buttonSpacing))
transition.updateFrame(node: self.buttonBackground, frame: buttonBounds)
transition.updateFrame(node: self.buttonLabel, frame: CGRect(origin: CGPoint(x: floor((buttonSize.width - labelSize.width) / 2.0), y: floor((buttonSize.height - labelSize.height) / 2.0)), size: buttonSize))
return SecureIdAuthContentLayout(height: inputContainerFrame.size.height, centerOffset: floor((inputContainerFrame.size.height) / 2.0))
}
func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
}
func didAppear() {
self.inputField.textField.becomeFirstResponder()
}
func willDisappear() {
self.inputField.textField.resignFirstResponder()
}
@objc private func buttonPressed() {
if self.isChecking {
return
}
if self.inputField.textField.text?.isEmpty ?? true {
self.inputField.layer.addShakeAnimation()
self.inputBackground.layer.addShakeAnimation()
self.hapticFeedback.error()
} else {
self.checkPassword(self.inputField.textField.text ?? "")
}
}
@objc private func inputButtonPressed() {
self.passwordHelp()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if self.isChecking {
return false
}
if self.clearOnce {
self.clearOnce = false
if range.length > string.count {
textField.text = ""
return false
}
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if !self.isChecking {
self.buttonPressed()
}
return false
}
func updateIsChecking(_ isChecking: Bool) {
self.isChecking = isChecking
self.inputField.alpha = isChecking ? 0.5 : 1.0
self.inputActivityNode.isHidden = !isChecking
self.inputButtonNode.isHidden = isChecking
}
func updateIsInvalid() {
self.clearOnce = true
}
}
@@ -0,0 +1,156 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
private let titleFont = Font.regular(14.0)
private let buttonFont = Font.regular(17.0)
final class SecureIdAuthPasswordSetupContentNode: ASDisplayNode, SecureIdAuthContentNode, UITextFieldDelegate {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let setupPassword: () -> Void
private let iconNode: ASImageNode
private let titleNode: ImmediateTextNode
private let buttonTopSeparatorNode: ASDisplayNode
private let buttonBackgroundNode: ASDisplayNode
private let buttonBottomSeparatorNode: ASDisplayNode
private let buttonHighlightedBackgroundNode: ASDisplayNode
private let buttonTextNode: ImmediateTextNode
private let buttonNode: HighlightableButtonNode
private var currentPendingConfirmation = false
private var validLayout: CGFloat?
init(theme: PresentationTheme, strings: PresentationStrings, setupPassword: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.setupPassword = setupPassword
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Secure ID/EmptyPasswordIcon"), color: theme.list.freeMonoIconColor)
self.titleNode = ImmediateTextNode()
self.titleNode.attributedText = NSAttributedString(string: strings.Passport_PasswordDescription, font: Font.regular(14.0), textColor: theme.list.freeTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.buttonTopSeparatorNode = ASDisplayNode()
self.buttonTopSeparatorNode.isLayerBacked = true
self.buttonTopSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.buttonBottomSeparatorNode = ASDisplayNode()
self.buttonBottomSeparatorNode.isLayerBacked = true
self.buttonBottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.buttonBackgroundNode = ASDisplayNode()
self.buttonBackgroundNode.isLayerBacked = true
self.buttonBackgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
self.buttonHighlightedBackgroundNode = ASDisplayNode()
self.buttonHighlightedBackgroundNode.isLayerBacked = true
self.buttonHighlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
self.buttonHighlightedBackgroundNode.alpha = 0.0
self.buttonTextNode = ImmediateTextNode()
self.buttonTextNode.attributedText = NSAttributedString(string: strings.Passport_PasswordCreate, font: buttonFont, textColor: theme.list.itemAccentColor)
self.buttonTextNode.maximumNumberOfLines = 1
self.buttonTextNode.textAlignment = .center
self.buttonNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.buttonBackgroundNode)
self.addSubnode(self.buttonTopSeparatorNode)
self.addSubnode(self.buttonBottomSeparatorNode)
self.addSubnode(self.buttonHighlightedBackgroundNode)
self.addSubnode(self.buttonTextNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonHighlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonHighlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.buttonHighlightedBackgroundNode.alpha = 0.0
strongSelf.buttonHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
func updatePendingConfirmation(_ pendingConfirmation: Bool) {
if pendingConfirmation != self.currentPendingConfirmation {
self.currentPendingConfirmation = pendingConfirmation
}
if !pendingConfirmation {
self.buttonTextNode.attributedText = NSAttributedString(string: self.strings.Passport_PasswordCreate, font: buttonFont, textColor: self.theme.list.itemAccentColor)
} else {
self.buttonTextNode.attributedText = NSAttributedString(string: self.strings.Passport_PasswordCompleteSetup, font: buttonFont, textColor: self.theme.list.itemAccentColor)
}
if let width = self.validLayout {
let _ = self.updateLayout(width: width, transition: .immediate)
}
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> SecureIdAuthContentLayout {
let transition = self.validLayout == nil ? .immediate : transition
self.validLayout = width
var iconSize = self.iconNode.image?.size ?? CGSize()
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((width - iconSize.width) / 2.0), y: -50.0), size: iconSize))
iconSize.height = max(0.0, iconSize.height - 100.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude))
let buttonTitleSize = self.buttonTextNode.updateLayout(CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude))
let buttonSize = CGSize(width: width, height: 44.0)
let iconSpacing: CGFloat = 30.0
let titleSpacing: CGFloat = 24.0
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: iconSize.height + iconSpacing), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let buttonFrame = CGRect(origin: CGPoint(x: 0.0, y: titleFrame.maxY + titleSpacing), size: buttonSize)
transition.updateFrame(node: self.buttonTopSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonFrame.minY), size: CGSize(width: width, height: UIScreenPixel)))
transition.updateFrame(node: self.buttonBottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonFrame.maxY - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame)
transition.updateFrame(node: self.buttonHighlightedBackgroundNode, frame: buttonFrame)
transition.updateFrame(node: self.buttonTextNode, frame: CGRect(origin: CGPoint(x: floor((buttonSize.width - buttonTitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
let contentHeight = buttonFrame.maxY
return SecureIdAuthContentLayout(height: contentHeight, centerOffset: floor(contentHeight / 2.0))
}
func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
}
func didAppear() {
}
func willDisappear() {
}
@objc private func buttonPressed() {
self.setupPassword()
}
}
@@ -0,0 +1,160 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
import AlertUI
import PresentationDataUtils
enum SecureIdDocumentFormScrollToSubject {
case selfie
case translation
}
enum SecureIdDocumentFormRequestedData {
case identity(details: ParsedRequestedPersonalDetails?, document: SecureIdRequestedIdentityDocument?, selfie: Bool, translations: Bool)
case address(details: Bool, document: SecureIdRequestedAddressDocument?, translations: Bool)
}
final class SecureIdDocumentFormController: FormController<SecureIdDocumentFormState, SecureIdDocumentFormControllerNodeInitParams, SecureIdDocumentFormControllerNode> {
private let context: AccountContext
private var presentationData: PresentationData
private let updatedValues: ([SecureIdValueWithContext]) -> Void
private let secureIdContext: SecureIdAccessContext
private let requestedData: SecureIdDocumentFormRequestedData
private let requestOptionalData: Bool
private let primaryLanguageByCountry: [String: String]
private var values: [SecureIdValueWithContext]
private let scrollTo: SecureIdDocumentFormScrollToSubject?
private var doneItem: UIBarButtonItem?
init(context: AccountContext, secureIdContext: SecureIdAccessContext, requestedData: SecureIdDocumentFormRequestedData, requestOptionalData: Bool = false, scrollTo: SecureIdDocumentFormScrollToSubject? = nil, primaryLanguageByCountry: [String: String], values: [SecureIdValueWithContext], updatedValues: @escaping ([SecureIdValueWithContext]) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.secureIdContext = secureIdContext
self.requestedData = requestedData
self.requestOptionalData = requestOptionalData
self.primaryLanguageByCountry = primaryLanguageByCountry
self.values = values
self.updatedValues = updatedValues
self.scrollTo = scrollTo
super.init(initParams: SecureIdDocumentFormControllerNodeInitParams(context: context, secureIdContext: secureIdContext), presentationData: self.presentationData)
self.navigationPresentation = .modal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
switch requestedData {
case let .identity(_, document, _, _):
if let document = document {
switch document {
case .passport:
self.title = self.presentationData.strings.Passport_Identity_TypePassport
case .internalPassport:
self.title = self.presentationData.strings.Passport_Identity_TypeInternalPassport
case .driversLicense:
self.title = self.presentationData.strings.Passport_Identity_TypeDriversLicense
case .idCard:
self.title = self.presentationData.strings.Passport_Identity_TypeIdentityCard
}
} else {
self.title = self.presentationData.strings.Passport_Identity_TypePersonalDetails
}
case let .address(_, document, _):
if let document = document {
switch document {
case .passportRegistration:
self.title = self.presentationData.strings.Passport_Address_TypePassportRegistration
case .temporaryRegistration:
self.title = self.presentationData.strings.Passport_Address_TypeTemporaryRegistration
case .utilityBill:
self.title = self.presentationData.strings.Passport_Address_TypeUtilityBill
case .bankStatement:
self.title = self.presentationData.strings.Passport_Address_TypeBankStatement
case .rentalAgreement:
self.title = self.presentationData.strings.Passport_Address_TypeRentalAgreement
}
} else {
self.title = self.presentationData.strings.Passport_Address_TypeResidentialAddress
}
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.rightBarButtonItem = self.doneItem
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
if self.controllerNode.hasUnsavedData() {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.Passport_DiscardMessageTitle, text: self.presentationData.strings.Passport_DiscardMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Passport_DiscardMessageAction, action: { [weak self] in
self?.dismiss()
})]), in: .window(.root))
} else {
self.dismiss()
}
}
@objc private func donePressed() {
self.controllerNode.save()
}
override func displayNodeDidLoad() {
super.displayNodeDidLoad()
self.controllerNode.actionInputStateUpdated = { [weak self] state in
if let strongSelf = self {
switch state {
case .inProgress:
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.presentationData.theme.rootController.navigationBar.controlColor))
case .saveAvailable, .saveNotAvailable:
if strongSelf.navigationItem.rightBarButtonItem !== strongSelf.doneItem {
strongSelf.navigationItem.rightBarButtonItem = strongSelf.doneItem
}
if case .saveAvailable = state {
strongSelf.doneItem?.isEnabled = true
} else {
strongSelf.doneItem?.isEnabled = false
}
}
}
}
self.controllerNode.completedWithValues = { [weak self] values in
if let strongSelf = self {
strongSelf.updatedValues(values ?? [])
strongSelf.dismiss()
}
}
self.controllerNode.dismiss = { [weak self] in
self?.dismiss()
}
var values: [SecureIdValueKey: SecureIdValueWithContext] = [:]
for value in self.values {
values[value.value.key] = value
}
self.controllerNode.updateInnerState(transition: .immediate, with: SecureIdDocumentFormState(requestedData: self.requestedData, values: values, requestOptionalData: self.requestOptionalData, primaryLanguageByCountry: self.primaryLanguageByCountry))
self.controllerNode.initiallyScrollTo = self.scrollTo
}
func addDocuments(type: SecureIdAddFileTarget, resources: [TelegramMediaResource], recognizedData: SecureIdRecognizedDocumentData?, removeDocumentId: SecureIdVerificationDocumentId?) {
self.controllerNode.addDocuments(type: type, resources: resources, recognizedData: recognizedData, removeDocumentId: removeDocumentId)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
import Foundation
import UIKit
import Display
import QuickLook
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import GalleryUI
struct SecureIdDocumentGalleryEntryLocation: Equatable {
let position: Int32
let totalCount: Int32
static func ==(lhs: SecureIdDocumentGalleryEntryLocation, rhs: SecureIdDocumentGalleryEntryLocation) -> Bool {
return lhs.position == rhs.position && lhs.totalCount == rhs.totalCount
}
}
struct SecureIdDocumentGalleryEntry: Equatable {
let index: Int32
let resource: TelegramMediaResource
let location: SecureIdDocumentGalleryEntryLocation
let error: String
static func ==(lhs: SecureIdDocumentGalleryEntry, rhs: SecureIdDocumentGalleryEntry) -> Bool {
return lhs.index == rhs.index && lhs.resource.isEqual(to: rhs.resource) && lhs.location == rhs.location && lhs.error == rhs.error
}
func item(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, delete: @escaping (TelegramMediaResource) -> Void) -> GalleryItem {
return SecureIdDocumentGalleryItem(context: context, theme: theme, strings: strings, secureIdContext: secureIdContext, itemId: self.index, resource: self.resource, caption: self.error, location: self.location, delete: {
delete(self.resource)
})
}
}
final class SecureIdDocumentGalleryControllerPresentationArguments {
let transitionArguments: (SecureIdDocumentGalleryEntry) -> GalleryTransitionArguments?
init(transitionArguments: @escaping (SecureIdDocumentGalleryEntry) -> GalleryTransitionArguments?) {
self.transitionArguments = transitionArguments
}
}
class SecureIdDocumentGalleryController: ViewController, StandalonePresentableController {
private var galleryNode: GalleryControllerNode {
return self.displayNode as! GalleryControllerNode
}
private let context: AccountContext
private let secureIdContext: SecureIdAccessContext
private var presentationData: PresentationData
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let disposable = MetaDisposable()
private var entries: [SecureIdDocumentGalleryEntry] = []
private var centralEntryIndex: Int?
private let centralItemTitle = Promise<String>()
private let centralItemTitleView = Promise<UIView?>()
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
private let centralItemAttributesDisposable = DisposableSet();
private let _hiddenMedia = Promise<SecureIdDocumentGalleryEntry?>(nil)
var hiddenMedia: Signal<SecureIdDocumentGalleryEntry?, NoError> {
return self._hiddenMedia.get()
}
private let replaceRootController: (ViewController, Promise<Bool>?) -> Void
var deleteResource: ((TelegramMediaResource) -> Void)?
init(context: AccountContext, secureIdContext: SecureIdAccessContext, entries: [SecureIdDocumentGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void) {
self.context = context
self.secureIdContext = secureIdContext
self.replaceRootController = replaceRootController
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = backItem
self.statusBar.statusBarStyle = .White
let entriesSignal: Signal<[SecureIdDocumentGalleryEntry], NoError> = .single(entries)
self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in
if let strongSelf = self {
strongSelf.entries = entries
strongSelf.centralEntryIndex = centralIndex
if strongSelf.isViewLoaded {
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({
$0.item(context: context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, secureIdContext: strongSelf.secureIdContext, delete: { resource in
self?.deleteItem(resource)
})
}), centralItemIndex: centralIndex)
let ready = (strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void()))) |> afterNext { [weak strongSelf] _ in
strongSelf?.didSetReady = true
}
strongSelf._ready.set(ready |> map { true })
}
}
}))
self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in
self?.navigationItem.title = title
}))
self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in
self?.navigationItem.titleView = titleView
}))
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in
self?.galleryNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
self.centralItemAttributesDisposable.dispose()
}
@objc func donePressed() {
self.dismiss(forceAway: false)
}
private func dismiss(forceAway: Bool) {
var animatedOutNode = true
var animatedOutInterface = false
let completion = { [weak self] in
if animatedOutNode && animatedOutInterface {
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? SecureIdDocumentGalleryControllerPresentationArguments {
if !self.entries.isEmpty {
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
}
}
}
self.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
}
override func loadDisplayNode() {
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
}
}, pushController: { _ in
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
}, controller: { [weak self] in
return self
})
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.galleryNode.statusBar = self.statusBar
self.galleryNode.navigationBar = self.navigationBar
self.galleryNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? SecureIdDocumentGalleryControllerPresentationArguments {
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
}
}
}
return nil
}
self.galleryNode.dismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var hiddenItem: SecureIdDocumentGalleryEntry?
if let index = index {
hiddenItem = strongSelf.entries[index]
if let node = strongSelf.galleryNode.pager.centralItemNode() {
strongSelf.centralItemTitle.set(node.title())
strongSelf.centralItemTitleView.set(node.titleView())
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
strongSelf.centralItemFooterContentNode.set(node.footerContent())
}
}
if strongSelf.didSetReady {
strongSelf._hiddenMedia.set(.single(hiddenItem))
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var nodeAnimatesItself = false
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? SecureIdDocumentGalleryControllerPresentationArguments {
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleView.set(centralItemNode.titleView())
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) {
nodeAnimatesItself = true
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
self._hiddenMedia.set(.single(self.entries[centralItemNode.index]))
}
}
self.galleryNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false)
}
private var firstLayout = true
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
if firstLayout {
firstLayout = false
self.galleryNode.pager.replaceItems(self.entries.map({
$0.item(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, secureIdContext: self.secureIdContext, delete: { [weak self] resource in
self?.deleteItem(resource)
})
}), centralItemIndex: self.centralEntryIndex)
let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
self?.didSetReady = true
}
self._ready.set(ready |> map { true })
}
}
private func deleteItem(_ resource: TelegramMediaResource) {
self.deleteResource?(resource)
self.dismiss(forceAway: true)
}
}
@@ -0,0 +1,162 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import Photos
import TelegramPresentationData
import AccountContext
import GalleryUI
import AppBundle
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let textFont = Font.regular(16.0)
private let titleFont = Font.medium(15.0)
private let dateFont = Font.regular(14.0)
final class SecureIdDocumentGalleryFooterContentNode: GalleryFooterContentNode {
private let context: AccountContext
private var theme: PresentationTheme
private var strings: PresentationStrings
private let deleteButton: UIButton
private let textNode: ASTextNode
private let authorNameNode: ASTextNode
private let dateNode: ASTextNode
private var currentDateText: String?
private var currentMessageText: String?
private var currentDocument: SecureIdVerificationDocument?
var delete: (() -> Void)?
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
self.context = context
self.theme = theme
self.strings = strings
self.deleteButton = UIButton()
self.deleteButton.setImage(deleteImage, for: [.normal])
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.authorNameNode = ASTextNode()
self.authorNameNode.maximumNumberOfLines = 1
self.authorNameNode.isUserInteractionEnabled = false
self.authorNameNode.displaysAsynchronously = false
self.dateNode = ASTextNode()
self.dateNode.maximumNumberOfLines = 1
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = false
super.init()
self.view.addSubview(self.deleteButton)
self.addSubnode(self.textNode)
self.addSubnode(self.authorNameNode)
self.addSubnode(self.dateNode)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
}
deinit {
}
func setup(caption: String) {
let dateText: String? = nil// = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, timeFormat: .regular, timestamp: $0) }
if self.currentMessageText != caption || self.currentDateText != dateText {
self.currentMessageText = caption
if caption.isEmpty {
self.textNode.isHidden = true
self.textNode.attributedText = nil
} else {
self.textNode.isHidden = false
self.textNode.attributedText = NSAttributedString(string: caption, font: textFont, textColor: UIColor(rgb: 0xcf3030))
}
self.authorNameNode.attributedText = nil
if let dateText = dateText {
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
} else {
self.dateNode.attributedText = nil
}
self.requestLayout?(.immediate)
}
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let width = size.width
var panelHeight: CGFloat = 44.0 + bottomInset
panelHeight += contentInset
if !self.textNode.isHidden {
let sideInset: CGFloat = 8.0 + leftInset
let topInset: CGFloat = 8.0
let textBottomInset: CGFloat = 8.0 + contentInset
let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
panelHeight += textSize.height + topInset + textBottomInset
self.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize)
}
self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude))
let dateSize = self.dateNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
if authorNameSize.height.isZero {
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)
} else {
let labelsSpacing: CGFloat = 0.0
self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
}
return panelHeight
}
override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: 0.0, y: self.bounds.size.height - fromHeight))
self.textNode.alpha = 1.0
self.dateNode.alpha = 1.0
self.authorNameNode.alpha = 1.0
self.deleteButton.alpha = 1.0
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateFrame(node: self.textNode, frame: self.textNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
self.textNode.alpha = 0.0
self.dateNode.alpha = 0.0
self.authorNameNode.alpha = 0.0
self.deleteButton.alpha = 0.0
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { _ in
completion()
})
}
@objc func deleteButtonPressed() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
let items: [ActionSheetItem] = [
ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.delete?()
})
]
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controllerInteraction?.presentController(actionSheet, nil)
}
}
@@ -0,0 +1,223 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import PhotoResources
import GalleryUI
class SecureIdDocumentGalleryItem: GalleryItem {
var id: AnyHashable {
return self.itemId
}
let itemId: AnyHashable
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let secureIdContext: SecureIdAccessContext
let resource: TelegramMediaResource
let caption: String
let location: SecureIdDocumentGalleryEntryLocation
let delete: () -> Void
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, itemId: AnyHashable, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) {
self.itemId = itemId
self.context = context
self.theme = theme
self.strings = strings
self.secureIdContext = secureIdContext
self.resource = resource
self.caption = caption
self.location = location
self.delete = delete
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = SecureIdDocumentGalleryItemNode(context: self.context, theme: self.theme, strings: self.strings)
node.setResource(secureIdContext: self.secureIdContext, resource: self.resource)
node._title.set(.single(self.strings.Items_NOfM("\(self.location.position + 1)", "\(self.location.totalCount)").string))
node.setCaption(self.caption)
node.delete = self.delete
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? SecureIdDocumentGalleryItemNode {
node._title.set(.single(self.strings.Items_NOfM("\(self.location.position + 1)", "\(self.location.totalCount)").string))
node.setCaption(self.caption)
node.delete = self.delete
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let imageNode: TransformImageNode
fileprivate let _ready = Promise<Void>()
fileprivate let _title = Promise<String>()
private let footerContentNode: SecureIdDocumentGalleryFooterContentNode
private var contextAndMedia: (AccountContext, SecureIdAccessContext, TelegramMediaResource)?
private var fetchDisposable = MetaDisposable()
var delete: (() -> Void)? {
didSet {
self.footerContentNode.delete = self.delete
}
}
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
self.context = context
self.imageNode = TransformImageNode()
self.footerContentNode = SecureIdDocumentGalleryFooterContentNode(context: context, theme: theme, strings: strings)
super.init()
self.imageNode.imageUpdated = { [weak self] _ in
self?._ready.set(.single(Void()))
}
self.imageNode.view.contentMode = .scaleAspectFill
self.imageNode.clipsToBounds = true
}
deinit {
self.fetchDisposable.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
fileprivate func setCaption(_ caption: String) {
self.footerContentNode.setup(caption: caption)
}
fileprivate func setResource(secureIdContext: SecureIdAccessContext, resource: TelegramMediaResource) {
if self.contextAndMedia == nil || !self.contextAndMedia!.2.isEqual(to: resource) {
let displaySize = CGSize(width: 1280.0, height: 1280.0)
//self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.setSignal(securePhotoInternal(account: context.account, resource: resource, accessContext: secureIdContext) |> beforeNext { [weak self] value in
Queue.mainQueue().async {
if let strongSelf = self {
if let size = value.0(), strongSelf.zoomableContent?.0 != size {
strongSelf.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()))()
strongSelf.zoomableContent = (size, strongSelf.imageNode)
}
}
}
} |> map { value in
return value.1
})
self.zoomableContent = (displaySize, self.imageNode)
self.fetchDisposable.set(context.account.postbox.mediaBox.fetchedResource(resource, parameters: nil).start())
}
self.contextAndMedia = (context, secureIdContext, resource)
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view)
let copyView = node.2().0!
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
self.imageNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.imageNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
transformedFrame.origin = CGPoint()
self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let copyView = node.2().0!
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
self.imageNode.layer.animatePosition(from: self.imageNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
}
@@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
private func stringForDocumentType(_ type: SecureIdRequestedIdentityDocument, strings: PresentationStrings) -> String {
switch type {
case .passport:
return strings.Passport_Identity_TypePassport
case .internalPassport:
return strings.Passport_Identity_TypeInternalPassport
case .idCard:
return strings.Passport_Identity_TypeIdentityCard
case .driversLicense:
return strings.Passport_Identity_TypeDriversLicense
}
}
private func stringForDocumentType(_ type: SecureIdRequestedAddressDocument, strings: PresentationStrings) -> String {
switch type {
case .rentalAgreement:
return strings.Passport_Address_TypeRentalAgreement
case .bankStatement:
return strings.Passport_Address_TypeBankStatement
case .passportRegistration:
return strings.Passport_Address_TypePassportRegistration
case .temporaryRegistration:
return strings.Passport_Address_TypeTemporaryRegistration
case .utilityBill:
return strings.Passport_Address_TypeUtilityBill
}
}
func documentSelectionItemsForField(field: SecureIdParsedRequestedFormField, strings: PresentationStrings) -> [(String, SecureIdDocumentFormRequestedData)] {
switch field {
case let .identity(personalDetails, document):
var result: [(String, SecureIdDocumentFormRequestedData)] = []
if let document = document {
switch document {
case let .just(type):
result.append((stringForDocumentType(type.document, strings: strings), .identity(details: personalDetails, document: type.document, selfie: type.selfie, translations: type.translation)))
case let .oneOf(types):
for type in types.sorted(by: { $0.document.rawValue < $1.document.rawValue }) {
result.append((stringForDocumentType(type.document, strings: strings), .identity(details: personalDetails, document: type.document, selfie: type.selfie, translations: type.translation)))
}
}
} else if let personalDetails = personalDetails {
result.append((strings.Passport_Identity_TypePersonalDetails, .identity(details: personalDetails, document: nil, selfie: false, translations: false)))
}
return result
case let .address(addressDetails, document):
var result: [(String, SecureIdDocumentFormRequestedData)] = []
if let document = document {
switch document {
case let .just(type):
result.append((stringForDocumentType(type.document, strings: strings), .address(details: addressDetails, document: type.document, translations: type.translation)))
case let .oneOf(types):
for type in types.sorted(by: { $0.document.rawValue < $1.document.rawValue }) {
result.append((stringForDocumentType(type.document, strings: strings), .address(details: addressDetails, document: type.document, translations: type.translation)))
}
}
} else if addressDetails {
result.append((strings.Passport_Address_TypeResidentialAddress, .address(details: true, document: nil, translations: false)))
}
return result
default:
return []
}
}
final class SecureIdDocumentTypeSelectionController: ActionSheetController {
private var presentationDisposable: Disposable?
private let completion: (SecureIdDocumentFormRequestedData) -> Void
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, field: SecureIdParsedRequestedFormField, currentValues: [SecureIdValueWithContext], completion: @escaping (SecureIdDocumentFormRequestedData) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
self.completion = completion
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
self._ready.set(.single(true))
var items: [ActionSheetItem] = []
for (title, data) in documentSelectionItemsForField(field: field, strings: strings) {
items.append(ActionSheetButtonItem(title: title, action: { [weak self] in
self?.dismissAnimated()
completion(data)
}))
}
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
public struct SecureIdLocalImageResourceId {
public let id: Int64
public var uniqueId: String {
return "secure-id-local-\(self.id)"
}
public var hashValue: Int {
return self.id.hashValue
}
}
public class SecureIdLocalImageResource: TelegramMediaResource {
public let localId: Int64
public let source: TelegramMediaResource
public var size: Int64? {
return nil
}
public init(localId: Int64, source: TelegramMediaResource) {
self.localId = localId
self.source = source
}
public required init(decoder: PostboxDecoder) {
self.localId = decoder.decodeInt64ForKey("i", orElse: 0)
self.source = decoder.decodeObjectForKey("s") as! TelegramMediaResource
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.localId, forKey: "i")
encoder.encodeObject(self.source, forKey: "s")
}
public var id: MediaResourceId {
return MediaResourceId(SecureIdLocalImageResourceId(id: self.localId).uniqueId)
}
public func isEqual(to: MediaResource) -> Bool {
if let to = to as? SecureIdLocalImageResource {
return self.localId == to.localId && self.source.isEqual(to:to.source)
} else {
return false
}
}
}
private final class Buffer {
var data = Data()
}
public func fetchSecureIdLocalImageResource(postbox: Postbox, resource: SecureIdLocalImageResource) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return Signal { subscriber in
guard let fetchResource = postbox.mediaBox.fetchResource else {
return EmptyDisposable
}
subscriber.putNext(.reset)
let fetch = fetchResource(resource.source, .single([(0 ..< Int64.max, .default)]), nil)
let buffer = Atomic<Buffer>(value: Buffer())
let disposable = fetch.start(next: { result in
switch result {
case .reset:
let _ = buffer.with { buffer in
buffer.data.count = 0
}
case .resourceSizeUpdated:
break
case .progressUpdated:
break
case let .moveLocalFile(path):
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let _ = buffer.with { buffer in
buffer.data = data
}
let _ = try? FileManager.default.removeItem(atPath: path)
}
case let .moveTempFile(file):
if let data = try? Data(contentsOf: URL(fileURLWithPath: file.path)) {
let _ = buffer.with { buffer in
buffer.data = data
}
}
TempBox.shared.dispose(file)
case let .copyLocalItem(item):
let tempFile = TempBox.shared.tempFile(fileName: "file")
if item.copyTo(url: URL(fileURLWithPath: tempFile.path)) {
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path)) {
let _ = buffer.with { buffer in
buffer.data = data
}
}
}
TempBox.shared.dispose(tempFile)
case let .replaceHeader(data, range):
let _ = buffer.with { buffer in
if buffer.data.count < range.count {
buffer.data.count = range.count
}
buffer.data.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
data.copyBytes(to: bytes, from: Int(range.lowerBound) ..< Int(range.upperBound))
}
}
case let .dataPart(resourceOffset, data, range, _):
let _ = buffer.with { buffer in
if buffer.data.count < Int(resourceOffset) + range.count {
buffer.data.count = Int(resourceOffset) + range.count
}
buffer.data.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
data.copyBytes(to: bytes.advanced(by: Int(resourceOffset)), from: Int(range.lowerBound) ..< Int(range.upperBound))
}
}
}
}, completed: {
let image = buffer.with { buffer -> UIImage? in
return UIImage(data: buffer.data)
}
if let image = image {
if let scaledImage = generateImage(image.size.fitted(CGSize(width: 2048.0, height: 2048.0)), contextGenerator: { size, context in
context.setBlendMode(.copy)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
}, scale: 1.0), let scaledData = scaledImage.jpegData(compressionQuality: 0.6) {
subscriber.putNext(.dataPart(resourceOffset: 0, data: scaledData, range: 0 ..< Int64(scaledData.count), complete: true))
subscriber.putCompletion()
}
}
})
return ActionDisposable {
disposable.dispose()
}
}
}
@@ -0,0 +1,116 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
public enum SecureIdPlaintextFormType {
case phone
case email
}
public final class SecureIdPlaintextFormController: FormController<SecureIdPlaintextFormInnerState, SecureIdPlaintextFormControllerNodeInitParams, SecureIdPlaintextFormControllerNode> {
private let context: AccountContext
private var presentationData: PresentationData
private let updatedValue: (SecureIdValueWithContext?) -> Void
private let secureIdContext: SecureIdAccessContext
private let type: SecureIdPlaintextFormType
private var immediatelyAvailableValue: SecureIdValue?
private var nextItem: UIBarButtonItem?
private var doneItem: UIBarButtonItem?
public init(context: AccountContext, secureIdContext: SecureIdAccessContext, type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?, updatedValue: @escaping (SecureIdValueWithContext?) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.secureIdContext = secureIdContext
self.type = type
self.immediatelyAvailableValue = immediatelyAvailableValue
self.updatedValue = updatedValue
super.init(initParams: SecureIdPlaintextFormControllerNodeInitParams(context: context, secureIdContext: secureIdContext), presentationData: self.presentationData)
self.navigationPresentation = .modal
switch type {
case .phone:
self.title = self.presentationData.strings.Passport_Phone_Title
case .email:
self.title = self.presentationData.strings.Passport_Email_Title
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.nextItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.donePressed))
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.rightBarButtonItem = doneItem
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func donePressed() {
self.controllerNode.save()
}
override public func displayNodeDidLoad() {
super.displayNodeDidLoad()
self.controllerNode.actionInputStateUpdated = { [weak self] state in
if let strongSelf = self {
switch state {
case .inProgress:
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: strongSelf.presentationData.theme.rootController.navigationBar.controlColor))
case .nextAvailable, .nextNotAvailable:
if strongSelf.navigationItem.rightBarButtonItem !== strongSelf.nextItem {
strongSelf.navigationItem.rightBarButtonItem = strongSelf.nextItem
}
if case .nextAvailable = state {
strongSelf.nextItem?.isEnabled = true
} else {
strongSelf.nextItem?.isEnabled = false
}
case .saveAvailable, .saveNotAvailable:
if strongSelf.navigationItem.rightBarButtonItem !== strongSelf.doneItem {
strongSelf.navigationItem.rightBarButtonItem = strongSelf.doneItem
}
if case .saveAvailable = state {
strongSelf.doneItem?.isEnabled = true
} else {
strongSelf.doneItem?.isEnabled = false
}
}
}
}
self.controllerNode.completedWithValue = { [weak self] valueWithContext in
if let strongSelf = self {
strongSelf.updatedValue(valueWithContext)
strongSelf.dismiss()
}
}
self.controllerNode.dismiss = { [weak self] in
self?.dismiss()
}
self.controllerNode.updateInnerState(transition: .immediate, with: SecureIdPlaintextFormInnerState(type: self.type, immediatelyAvailableValue: self.immediatelyAvailableValue))
}
public func applyPhoneCode(_ code: Int) {
self.controllerNode.applyPhoneCode(code)
}
}
@@ -0,0 +1,987 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AlertUI
import PresentationDataUtils
import CountrySelectionUI
import PhoneNumberFormat
private func cleanPhoneNumber(_ text: String?) -> String {
var cleanNumber = ""
if let text = text {
for c in text {
if c >= "0" && c <= "9" {
cleanNumber += String(c)
}
}
}
return cleanNumber
}
public final class SecureIdPlaintextFormParams {
fileprivate let openCountrySelection: () -> Void
fileprivate let updateTextField: (SecureIdPlaintextFormTextField, String) -> Void
fileprivate let usePhone: (String) -> Void
fileprivate let useEmailAddress: (String) -> Void
fileprivate let save: () -> Void
fileprivate init(openCountrySelection: @escaping () -> Void, updateTextField: @escaping (SecureIdPlaintextFormTextField, String) -> Void, usePhone: @escaping (String) -> Void, useEmailAddress: @escaping (String) -> Void, save: @escaping () -> Void) {
self.openCountrySelection = openCountrySelection
self.updateTextField = updateTextField
self.usePhone = usePhone
self.useEmailAddress = useEmailAddress
self.save = save
}
}
private struct PhoneInputState {
var countryCode: String
var number: String
var countryId: String
func isEqual(to: PhoneInputState) -> Bool {
if self.countryCode != to.countryCode {
return false
}
if self.number != to.number {
return false
}
if self.countryId != to.countryId {
return false
}
return true
}
}
private struct PhoneVerifyState {
let phone: String
let payload: SecureIdPreparePhoneVerificationPayload
var code: String
func isEqual(to: PhoneVerifyState) -> Bool {
if self.code != to.code {
return false
}
return true
}
}
private enum SecureIdPlaintextFormPhoneState {
case input(PhoneInputState)
case verify(PhoneVerifyState)
func isEqual(to: SecureIdPlaintextFormPhoneState) -> Bool {
switch self {
case let .input(lhsInput):
if case let .input(rhsInput) = to, lhsInput.isEqual(to: rhsInput) {
return true
} else {
return false
}
case let .verify(lhsInput):
if case let .verify(rhsInput) = to, lhsInput.isEqual(to: rhsInput) {
return true
} else {
return false
}
}
}
func isComplete() -> Bool {
switch self {
case let .input(input):
if input.countryCode.isEmpty {
return false
}
if input.number.isEmpty {
return false
}
return true
case let .verify(verify):
if verify.code.isEmpty {
return false
}
return true
}
}
}
private struct EmailInputState {
var email: String
func isEqual(to: EmailInputState) -> Bool {
if self.email != to.email {
return false
}
return true
}
}
private struct EmailVerifyState {
let email: String
let payload: SecureIdPrepareEmailVerificationPayload
var code: String
func isEqual(to: EmailVerifyState) -> Bool {
if self.code != to.code {
return false
}
return true
}
}
private enum SecureIdPlaintextFormEmailState {
case input(EmailInputState)
case verify(EmailVerifyState)
func isEqual(to: SecureIdPlaintextFormEmailState) -> Bool {
switch self {
case let .input(lhsInput):
if case let .input(rhsInput) = to, lhsInput.isEqual(to: rhsInput) {
return true
} else {
return false
}
case let .verify(lhsInput):
if case let .verify(rhsInput) = to, lhsInput.isEqual(to: rhsInput) {
return true
} else {
return false
}
}
}
func isComplete() -> Bool {
switch self {
case let .input(input):
if input.email.isEmpty {
return false
}
return true
case let .verify(verify):
if verify.code.isEmpty {
return false
}
return true
}
}
}
private enum SecureIdPlaintextFormTextField {
case countryCode
case number
case code
case email
}
private enum SecureIdPlaintextFormDataState {
case phone(SecureIdPlaintextFormPhoneState)
case email(SecureIdPlaintextFormEmailState)
mutating func updateTextField(type: SecureIdPlaintextFormTextField, value: String) {
switch self {
case let .phone(phone):
switch phone {
case var .input(input):
switch type {
case .countryCode:
input.countryCode = value
case .number:
input.number = value
default:
break
}
self = .phone(.input(input))
case var .verify(verify):
switch type {
case .code:
verify.code = value
default:
break
}
self = .phone(.verify(verify))
}
case let .email(email):
switch email {
case var .input(input):
switch type {
case .email:
input.email = value
default:
break
}
self = .email(.input(input))
case var .verify(verify):
switch type {
case .code:
verify.code = value
default:
break
}
self = .email(.verify(verify))
}
}
}
func isEqual(to: SecureIdPlaintextFormDataState) -> Bool {
switch self {
case let .phone(lhsValue):
if case let .phone(rhsValue) = to, lhsValue.isEqual(to: rhsValue) {
return true
} else {
return false
}
case let .email(lhsValue):
if case let .email(rhsValue) = to, lhsValue.isEqual(to: rhsValue) {
return true
} else {
return false
}
}
}
}
private enum SecureIdPlaintextFormActionState {
case none
case saving
case deleting
}
enum SecureIdPlaintextFormInputState {
case nextAvailable
case nextNotAvailable
case saveAvailable
case saveNotAvailable
case inProgress
}
public struct SecureIdPlaintextFormInnerState: FormControllerInnerState {
fileprivate let previousValue: SecureIdValue?
fileprivate var data: SecureIdPlaintextFormDataState
fileprivate var actionState: SecureIdPlaintextFormActionState
public func isEqual(to: SecureIdPlaintextFormInnerState) -> Bool {
if !self.data.isEqual(to: to.data) {
return false
}
if self.actionState != to.actionState {
return false
}
return true
}
public func entries() -> [FormControllerItemEntry<SecureIdPlaintextFormEntry>] {
switch self.data {
case let .phone(phone):
var result: [FormControllerItemEntry<SecureIdPlaintextFormEntry>] = []
switch phone {
case let .input(input):
result.append(.spacer)
if let value = self.previousValue, case let .phone(phone) = value {
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhone(phone.phone)))
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhoneInfo))
result.append(.spacer)
result.append(.entry(SecureIdPlaintextFormEntry.numberInputHeader))
}
result.append(.entry(SecureIdPlaintextFormEntry.numberInput(countryCode: input.countryCode, number: input.number)))
result.append(.entry(SecureIdPlaintextFormEntry.numberInputInfo))
case let .verify(verify):
result.append(.spacer)
var codeLength: Int32 = 5
switch verify.payload.type {
case let .sms(length):
codeLength = length
case let .call(length):
codeLength = length
case let .otherSession(length):
codeLength = length
default:
break
}
result.append(.entry(SecureIdPlaintextFormEntry.numberCode(verify.code, codeLength)))
result.append(.entry(SecureIdPlaintextFormEntry.numberVerifyInfo))
}
return result
case let .email(email):
var result: [FormControllerItemEntry<SecureIdPlaintextFormEntry>] = []
switch email {
case let .input(input):
result.append(.spacer)
if let value = self.previousValue, case let .email(email) = value {
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailableEmail(email.email)))
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailableEmailInfo))
result.append(.spacer)
result.append(.entry(SecureIdPlaintextFormEntry.emailInputHeader))
}
result.append(.entry(SecureIdPlaintextFormEntry.emailAddress(input.email)))
result.append(.entry(SecureIdPlaintextFormEntry.emailInputInfo))
case let .verify(verify):
result.append(.spacer)
result.append(.entry(SecureIdPlaintextFormEntry.numberCode(verify.code, verify.payload.length)))
result.append(.entry(SecureIdPlaintextFormEntry.emailVerifyInfo(verify.email)))
}
return result
}
}
func actionInputState() -> SecureIdPlaintextFormInputState {
switch self.actionState {
case .deleting, .saving:
return .inProgress
default:
break
}
switch self.data {
case let .phone(phone):
switch phone {
case .input:
if !phone.isComplete() {
return .nextNotAvailable
} else {
return .nextAvailable
}
case .verify:
if !phone.isComplete() {
return .saveNotAvailable
} else {
return .saveAvailable
}
}
case let .email(email):
switch email {
case .input:
if !email.isComplete() {
return .nextNotAvailable
} else {
return .nextAvailable
}
case .verify:
if !email.isComplete() {
return .saveNotAvailable
} else {
return .saveAvailable
}
}
}
}
}
extension SecureIdPlaintextFormInnerState {
init(type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?) {
switch type {
case .phone:
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCodeAndId: (Int32, String) = (1, "US")
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {
if idAndName.0 == normalizedId {
countryCodeAndId = (Int32(code), idAndName.0.uppercased())
break
}
}
}
self.init(previousValue: immediatelyAvailableValue, data: .phone(.input(PhoneInputState(countryCode: "+\(countryCodeAndId.0)", number: "", countryId: countryCodeAndId.1))), actionState: .none)
case .email:
self.init(previousValue: immediatelyAvailableValue, data: .email(.input(EmailInputState(email: ""))), actionState: .none)
}
}
}
public enum SecureIdPlaintextFormEntryId: Hashable {
case immediatelyAvailablePhone
case immediatelyAvailablePhoneInfo
case numberInputHeader
case numberInput
case numberInputInfo
case numberCode
case numberVerifyInfo
case immediatelyAvailableEmail
case immediatelyAvailableEmailInfo
case emailInputHeader
case emailAddress
case emailInputInfo
case emailCode
case emailVerifyInfo
}
public enum SecureIdPlaintextFormEntry: FormControllerEntry {
case immediatelyAvailablePhone(String)
case immediatelyAvailablePhoneInfo
case numberInputHeader
case numberInput(countryCode: String, number: String)
case numberInputInfo
case numberCode(String, Int32)
case numberVerifyInfo
case immediatelyAvailableEmail(String)
case immediatelyAvailableEmailInfo
case emailInputHeader
case emailAddress(String)
case emailInputInfo
case emailCode(String)
case emailVerifyInfo(String)
public var stableId: SecureIdPlaintextFormEntryId {
switch self {
case .immediatelyAvailablePhone:
return .immediatelyAvailablePhone
case .immediatelyAvailablePhoneInfo:
return .immediatelyAvailablePhoneInfo
case .numberInputHeader:
return .numberInputHeader
case .numberInput:
return .numberInput
case .numberInputInfo:
return .numberInputInfo
case .numberCode:
return .numberCode
case .numberVerifyInfo:
return .numberVerifyInfo
case .immediatelyAvailableEmail:
return .immediatelyAvailableEmail
case .immediatelyAvailableEmailInfo:
return .immediatelyAvailableEmailInfo
case .emailInputHeader:
return .emailInputHeader
case .emailAddress:
return .emailAddress
case .emailInputInfo:
return .emailInputInfo
case .emailCode:
return .emailCode
case .emailVerifyInfo:
return .emailVerifyInfo
}
}
public func isEqual(to: SecureIdPlaintextFormEntry) -> Bool {
switch self {
case let .immediatelyAvailablePhone(value):
if case .immediatelyAvailablePhone(value) = to {
return true
} else {
return false
}
case .immediatelyAvailablePhoneInfo:
if case .immediatelyAvailablePhoneInfo = to {
return true
} else {
return false
}
case .numberInputHeader:
if case .numberInputHeader = to {
return true
} else {
return false
}
case let .numberInput(countryCode, number):
if case .numberInput(countryCode, number) = to {
return true
} else {
return false
}
case .numberInputInfo:
if case .numberInputInfo = to {
return true
} else {
return false
}
case let .numberCode(code, length):
if case .numberCode(code, length) = to {
return true
} else {
return false
}
case .numberVerifyInfo:
if case .numberVerifyInfo = to {
return true
} else {
return false
}
case let .immediatelyAvailableEmail(value):
if case .immediatelyAvailableEmail(value) = to {
return true
} else {
return false
}
case .immediatelyAvailableEmailInfo:
if case .immediatelyAvailableEmailInfo = to {
return true
} else {
return false
}
case .emailInputHeader:
if case .emailInputHeader = to {
return true
} else {
return false
}
case let .emailAddress(code):
if case .emailAddress(code) = to {
return true
} else {
return false
}
case .emailInputInfo:
if case .emailInputInfo = to {
return true
} else {
return false
}
case let .emailCode(code):
if case .emailCode(code) = to {
return true
} else {
return false
}
case let .emailVerifyInfo(address):
if case .emailVerifyInfo(address) = to {
return true
} else {
return false
}
}
}
public func item(params: SecureIdPlaintextFormParams, strings: PresentationStrings) -> FormControllerItem {
switch self {
case let .immediatelyAvailablePhone(value):
return FormControllerActionItem(type: .accent, title: strings.Passport_Phone_UseTelegramNumber(formatPhoneNumber(value)).string, activated: {
params.usePhone(value)
})
case .immediatelyAvailablePhoneInfo:
return FormControllerTextItem(text: strings.Passport_Phone_UseTelegramNumberHelp)
case .numberInputHeader:
return FormControllerHeaderItem(text: strings.Passport_Phone_EnterOtherNumber)
case let .numberInput(countryCode, number):
var countryName = ""
if let codeNumber = Int(countryCode), let codeId = AuthorizationSequenceCountrySelectionController.lookupCountryIdByCode(codeNumber) {
countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(codeId, strings: strings) ?? ""
}
return SecureIdValueFormPhoneItem(countryCode: countryCode, number: number, countryName: countryName, openCountrySelection: {
params.openCountrySelection()
}, updateCountryCode: { value in
params.updateTextField(.countryCode, value)
}, updateNumber: { value in
params.updateTextField(.number, value)
})
case .numberInputInfo:
return FormControllerTextItem(text: strings.Passport_Phone_Help)
case let .numberCode(code, length):
return FormControllerTextInputItem(title: strings.ChangePhoneNumberCode_CodePlaceholder, text: code, placeholder: strings.ChangePhoneNumberCode_CodePlaceholder, type: .number, textUpdated: { value in
params.updateTextField(.code, value)
if value.count == length {
params.save()
}
}, returnPressed: {
})
case .numberVerifyInfo:
return FormControllerTextItem(text: strings.ChangePhoneNumberCode_Help)
case let .immediatelyAvailableEmail(value):
return FormControllerActionItem(type: .accent, title: strings.Passport_Email_UseTelegramEmail(value).string, activated: {
params.useEmailAddress(value)
})
case .immediatelyAvailableEmailInfo:
return FormControllerTextItem(text: strings.Passport_Email_UseTelegramEmailHelp)
case .emailInputHeader:
return FormControllerHeaderItem(text: strings.Passport_Email_EnterOtherEmail)
case let .emailAddress(address):
return FormControllerTextInputItem(title: strings.TwoStepAuth_Email, text: address, placeholder: strings.Passport_Email_EmailPlaceholder, type: .email, textUpdated: { value in
params.updateTextField(.email, value)
}, returnPressed: {
params.save()
})
case .emailInputInfo:
return FormControllerTextItem(text: strings.Passport_Email_Help)
case let .emailCode(code):
return FormControllerTextInputItem(title: strings.TwoStepAuth_RecoveryCode, text: code, placeholder: strings.TwoStepAuth_RecoveryCode, type: .number, textUpdated: { value in
params.updateTextField(.code, value)
}, returnPressed: {
})
case let .emailVerifyInfo(address):
return FormControllerTextItem(text: strings.Passport_Email_CodeHelp(address).string)
}
}
}
public struct SecureIdPlaintextFormControllerNodeInitParams {
let context: AccountContext
let secureIdContext: SecureIdAccessContext
}
private enum SecureIdPlaintextFormNavigatonTransition {
case none
case push
}
public final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlaintextFormControllerNodeInitParams, SecureIdPlaintextFormInnerState> {
private var _itemParams: SecureIdPlaintextFormParams?
override public var itemParams: SecureIdPlaintextFormParams {
return self._itemParams!
}
private var theme: PresentationTheme
private var strings: PresentationStrings
private let context: AccountContext
private let secureIdContext: SecureIdAccessContext
var actionInputStateUpdated: ((SecureIdPlaintextFormInputState) -> Void)?
var completedWithValue: ((SecureIdValueWithContext?) -> Void)?
var dismiss: (() -> Void)?
private let actionDisposable = MetaDisposable()
required public init(initParams: SecureIdPlaintextFormControllerNodeInitParams, presentationData: PresentationData) {
self.theme = presentationData.theme
self.strings = presentationData.strings
self.context = initParams.context
self.secureIdContext = initParams.secureIdContext
super.init(initParams: initParams, presentationData: presentationData)
self._itemParams = SecureIdPlaintextFormParams(openCountrySelection: { [weak self] in
guard let strongSelf = self else {
return
}
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme, displayCodes: true)
controller.completeWithCountryCode = { code, _ in
if let strongSelf = self, var innerState = strongSelf.innerState {
innerState.data.updateTextField(type: .countryCode, value: "+\(code)")
strongSelf.updateInnerState(transition: .immediate, with: innerState)
}
}
strongSelf.view.endEditing(true)
strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, updateTextField: { [weak self] type, value in
guard let strongSelf = self else {
return
}
guard var innerState = strongSelf.innerState else {
return
}
innerState.data.updateTextField(type: type, value: value)
strongSelf.updateInnerState(transition: .immediate, with: innerState)
}, usePhone: { [weak self] value in
self?.savePhone(value)
}, useEmailAddress: { [weak self] value in
self?.saveEmailAddress(value)
}, save: { [weak self] in
self?.save()
})
}
deinit {
self.actionDisposable.dispose()
}
override func updateInnerState(transition: ContainedViewLayoutTransition, with innerState: SecureIdPlaintextFormInnerState) {
let previousActionInputState = self.innerState?.actionInputState()
super.updateInnerState(transition: transition, with: innerState)
let actionInputState = innerState.actionInputState()
if previousActionInputState != actionInputState {
self.actionInputStateUpdated?(actionInputState)
}
}
private func updateInnerState(transition: ContainedViewLayoutTransition, navigationTransition: SecureIdPlaintextFormNavigatonTransition, with innerState: SecureIdPlaintextFormInnerState) {
if case .push = navigationTransition {
if let snapshotView = self.scrollNode.view.snapshotContentTree() {
snapshotView.frame = self.scrollNode.view.frame.offsetBy(dx: 0.0, dy: self.scrollNode.view.contentInset.top)
self.scrollNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.scrollNode.view)
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.scrollNode.view.bounds.width, y: 0.0), duration: 0.25, removeOnCompletion: false, additive: true, completion : { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.scrollNode.view.layer.animatePosition(from: CGPoint(x: self.scrollNode.view.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, additive: true)
}
}
self.updateInnerState(transition: transition, with: innerState)
}
func activateMainInput() {
self.enumerateItemsAndEntries({ itemEntry, itemNode in
switch itemEntry {
case .emailAddress, .numberCode, .emailCode:
if let inputNode = itemNode as? FormControllerTextInputItemNode {
inputNode.activate()
}
return false
case .numberInput:
if let inputNode = itemNode as? SecureIdValueFormPhoneItemNode {
inputNode.activate()
}
return false
default:
return true
}
})
}
override func didAppear() {
self.activateMainInput()
}
func save() {
guard var innerState = self.innerState else {
return
}
guard case .none = innerState.actionState else {
return
}
switch innerState.data {
case let .phone(phone):
switch phone {
case let .input(input):
self.savePhone(input.countryCode + input.number)
return
case .verify:
self.verifyPhoneCode()
return
}
case let .email(email):
switch email {
case let .input(input):
self.saveEmailAddress(input.email)
return
case let .verify(verify):
guard case .saveAvailable = innerState.actionInputState() else {
return
}
innerState.actionState = .saving
self.updateInnerState(transition: .immediate, with: innerState)
self.actionDisposable.set((secureIdCommitEmailVerification(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, payload: verify.payload, code: verify.code)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
guard let innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
strongSelf.completedWithValue?(result)
}
}, error: { [weak self] error in
if let strongSelf = self {
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
strongSelf.updateInnerState(transition: .immediate, with: innerState)
let errorText: String
switch error {
case .generic:
errorText = strongSelf.strings.Login_UnknownError
case .flood:
errorText = strongSelf.strings.Login_CodeFloodError
case .invalid:
errorText = strongSelf.strings.Login_InvalidCodeError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
}
}))
return
}
}
}
private func savePhone(_ value: String) {
guard var innerState = self.innerState else {
return
}
guard case .none = innerState.actionState else {
return
}
innerState.actionState = .saving
let inputPhone = cleanPhoneNumber(value)
self.updateInnerState(transition: .immediate, with: innerState)
self.actionDisposable.set((secureIdPreparePhoneVerification(network: self.context.account.network, value: SecureIdPhoneValue(phone: inputPhone))
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
innerState.data = .phone(.verify(PhoneVerifyState(phone: inputPhone, payload: result, code: "")))
strongSelf.updateInnerState(transition: .immediate, navigationTransition: .push, with: innerState)
strongSelf.activateMainInput()
}
}, error: { [weak self] error in
if let strongSelf = self {
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
strongSelf.updateInnerState(transition: .immediate, with: innerState)
let errorText: String
switch error {
case .generic:
errorText = strongSelf.strings.Login_UnknownError
case .flood:
errorText = strongSelf.strings.Login_CodeFloodError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
}
}))
}
private func saveEmailAddress(_ value: String) {
guard var innerState = self.innerState else {
return
}
guard case .none = innerState.actionState else {
return
}
innerState.actionState = .saving
self.updateInnerState(transition: .immediate, with: innerState)
self.actionDisposable.set((saveSecureIdValue(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, value: .email(SecureIdEmailValue(email: value)), uploadedFiles: [:])
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.completedWithValue?(result)
}, error: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.actionDisposable.set((secureIdPrepareEmailVerification(network: strongSelf.context.account.network, value: SecureIdEmailValue(email: value))
|> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else {
return
}
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
innerState.data = .email(.verify(EmailVerifyState(email: value, payload: result, code: "")))
strongSelf.updateInnerState(transition: .immediate, navigationTransition: .push, with: innerState)
strongSelf.activateMainInput()
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
strongSelf.updateInnerState(transition: .immediate, with: innerState)
let errorText: String
switch error {
case .generic:
errorText = strongSelf.strings.Login_UnknownError
case .invalidEmail:
errorText = strongSelf.strings.TwoStepAuth_EmailInvalid
case .flood:
errorText = strongSelf.strings.Login_CodeFloodError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
}))
}))
}
private func verifyPhoneCode() {
guard var innerState = self.innerState else {
return
}
guard case let .phone(phone) = innerState.data, case let .verify(verify) = phone else {
return
}
guard case .saveAvailable = innerState.actionInputState() else {
return
}
innerState.actionState = .saving
self.updateInnerState(transition: .immediate, with: innerState)
self.actionDisposable.set((secureIdCommitPhoneVerification(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, payload: verify.payload, code: verify.code)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
guard let innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
strongSelf.completedWithValue?(result)
}
}, error: { [weak self] error in
if let strongSelf = self {
guard var innerState = strongSelf.innerState else {
return
}
guard case .saving = innerState.actionState else {
return
}
innerState.actionState = .none
strongSelf.updateInnerState(transition: .immediate, with: innerState)
let errorText: String
switch error {
case .generic:
errorText = strongSelf.strings.Login_UnknownError
case .flood:
errorText = strongSelf.strings.Login_CodeFloodError
case .invalid:
errorText = strongSelf.strings.Login_InvalidCodeError
}
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
}
}))
}
func applyPhoneCode(_ code: Int) {
guard var innerState = self.innerState else {
return
}
switch innerState.data {
case let .phone(phone):
switch phone {
case var .verify(verify):
let value = "\(code)"
verify.code = value
innerState.data = .phone(.verify(verify))
self.updateInnerState(transition: .immediate, with: innerState)
self.verifyPhoneCode()
default:
break
}
default:
break
}
}
}
@@ -0,0 +1,226 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import RadialStatusNode
import PhotoResources
private let textFont = Font.regular(16.0)
private let labelFont = Font.regular(13.0)
enum SecureIdValueFormFileItemLabel {
case timestamp
case error(String)
case text(String)
}
private enum RevealOptionKey: Int32 {
case delete
}
final class SecureIdValueFormFileItem: FormControllerItem {
let account: Account
let context: SecureIdAccessContext
let document: SecureIdVerificationDocument?
let placeholder: UIImage?
let title: String
let label: SecureIdValueFormFileItemLabel
let activated: () -> Void
let deleted: () -> Void
init(account: Account, context: SecureIdAccessContext, document: SecureIdVerificationDocument?, placeholder: UIImage?, title: String, label: SecureIdValueFormFileItemLabel, activated: @escaping () -> Void, deleted: @escaping () -> Void) {
self.account = account
self.context = context
self.document = document
self.placeholder = placeholder
self.title = title
self.label = label
self.activated = activated
self.deleted = deleted
}
func node() -> ASDisplayNode & FormControllerItemNode {
return SecureIdValueFormFileItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? SecureIdValueFormFileItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class SecureIdValueFormFileItemNode: FormEditableBlockItemNode<SecureIdValueFormFileItem> {
private let titleNode: ImmediateTextNode
private let labelNode: ImmediateTextNode
let imageNode: TransformImageNode
private let placeholderNode: ASImageNode
private let statusNode: RadialStatusNode
private(set) var item: SecureIdValueFormFileItem?
init() {
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.labelNode = ImmediateTextNode()
self.labelNode.maximumNumberOfLines = 1
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.imageNode = TransformImageNode()
self.imageNode.isUserInteractionEnabled = false
self.placeholderNode = ASImageNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.displayWithoutProcessing = true
self.placeholderNode.contentMode = .center
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
super.init(selectable: true, topSeparatorInset: .custom(92))
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.statusNode)
}
override func update(item: SecureIdValueFormFileItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
var resourceUpdated = false
if let previousItem = self.item {
if let previousDocument = previousItem.document, let document = item.document {
resourceUpdated = !previousDocument.resource.isEqual(to: document.resource)
} else if (previousItem.document != nil) != (item.document != nil) {
resourceUpdated = true
}
} else {
resourceUpdated = true
}
self.item = item
self.placeholderNode.image = item.placeholder
var progress: CGFloat?
if let document = item.document {
switch document {
case .remote:
break
case let .local(local):
if case let .uploading(value) = local.state {
progress = CGFloat(value)
}
}
self.imageNode.isHidden = false
self.placeholderNode.isHidden = true
self.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: .none, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)]))
} else {
self.imageNode.isHidden = true
self.placeholderNode.isHidden = false
self.setRevealOptions((left: [], right: []))
}
let progressState: RadialStatusNodeState
if let progress = progress {
progressState = .progress(color: .white, lineWidth: nil, value: progress, cancelEnabled: false, animateRotation: true)
} else {
progressState = .none
}
self.statusNode.transitionToState(progressState, completion: {})
let revealOffset = self.revealOffset
let imageSize = CGSize(width: 60.0, height: 44.0)
let progressSize: CGFloat = 32.0
let imageFrame = CGRect(origin: CGPoint(x: 10.0 + revealOffset, y: 10.0), size: imageSize)
transition.updateFrame(node: self.imageNode, frame: imageFrame)
if let image = self.placeholderNode.image {
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.width - image.size.width) / 2.0), y: imageFrame.minY + floor((imageFrame.height - image.size.height) / 2.0)), size: image.size))
}
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.width - progressSize) / 2.0), y: imageFrame.minY + floor((imageFrame.height - progressSize) / 2.0)), size: CGSize(width: progressSize, height: progressSize)))
let makeLayout = self.imageNode.asyncLayout()
makeLayout(TransformImageArguments(corners: ImageCorners(radius: 6.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))()
if resourceUpdated {
if let resource = item.document?.resource {
self.imageNode.setSignal(securePhoto(account: item.account, resource: resource, accessContext: item.context))
} else {
self.imageNode.setSignal(.single({ _ in return nil }))
}
}
let leftInset: CGFloat = 92.0
self.titleNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
switch item.label {
case .timestamp:
self.labelNode.maximumNumberOfLines = 1
if let document = item.document {
self.labelNode.attributedText = NSAttributedString(string: stringForFullDate(timestamp: document.timestamp, strings: strings, dateTimeFormat: dateTimeFormat), font: labelFont, textColor: theme.list.itemSecondaryTextColor)
}
case let .error(text):
self.labelNode.maximumNumberOfLines = 40
self.labelNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: theme.list.freeTextErrorColor)
case let .text(text):
self.labelNode.maximumNumberOfLines = 40
self.labelNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: theme.list.itemSecondaryTextColor)
}
let labelSize = self.labelNode.updateLayout(CGSize(width: width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude))
return (FormControllerItemPreLayout(aligningInset: 0.0), { params in
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset, y: 14.0), size: titleSize))
let labelFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset, y: 36.0), size: labelSize)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
return max(64.0, labelFrame.maxY + 8.0)
})
}
override func selected() {
self.item?.activated()
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let _ = self.item {
let progressSize: CGFloat = 32.0
let imageFrame = CGRect(origin: CGPoint(x: 10.0 + offset, y: self.imageNode.frame.minY), size: self.imageNode.frame.size)
transition.updateFrame(node: self.imageNode, frame: imageFrame)
if let image = self.placeholderNode.image {
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.width - image.size.width) / 2.0), y: self.placeholderNode.frame.minY), size: image.size))
}
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.width - progressSize) / 2.0), y: self.statusNode.frame.minY), size: self.statusNode.frame.size))
let leftInset: CGFloat = 92.0
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset, y: self.titleNode.frame.minY), size: self.titleNode.frame.size))
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: leftInset + offset, y: self.labelNode.frame.minY), size: self.labelNode.frame.size))
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
switch option.key {
case RevealOptionKey.delete.rawValue:
item.deleted()
default:
break
}
}
}
}
@@ -0,0 +1,182 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import PhoneInputNode
private let textFont = Font.regular(17.0)
private func countryButtonBackground(color: UIColor, separatorColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 45.0, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
context.setStrokeColor(separatorColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 46, topCapHeight: 1)
}
private func countryButtonHighlightedBackground(fillColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 45.0, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(fillColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 46, topCapHeight: 2)
}
final class SecureIdValueFormPhoneItem: FormControllerItem {
fileprivate let countryCode: String
fileprivate let number: String
fileprivate let countryName: String
fileprivate let openCountrySelection: () -> Void
fileprivate let updateCountryCode: (String) -> Void
fileprivate let updateNumber: (String) -> Void
init(countryCode: String, number: String, countryName: String, openCountrySelection: @escaping () -> Void, updateCountryCode: @escaping (String) -> Void, updateNumber: @escaping (String) -> Void) {
self.countryCode = countryCode
self.number = number
self.countryName = countryName
self.openCountrySelection = openCountrySelection
self.updateCountryCode = updateCountryCode
self.updateNumber = updateNumber
}
func node() -> ASDisplayNode & FormControllerItemNode {
return SecureIdValueFormPhoneItemNode()
}
func update(node: ASDisplayNode & FormControllerItemNode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
guard let node = node as? SecureIdValueFormPhoneItemNode else {
assertionFailure()
return (FormControllerItemPreLayout(aligningInset: 0.0), { _ in
return 0.0
})
}
return node.updateInternal(item: self, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
}
}
final class SecureIdValueFormPhoneItemNode: FormBlockItemNode<SecureIdValueFormPhoneItem> {
private let countryButton: ASButtonNode
private let phoneInputNode: PhoneInputNode
private var item: SecureIdValueFormPhoneItem?
private var theme: PresentationTheme?
init() {
self.countryButton = ASButtonNode()
self.phoneInputNode = PhoneInputNode(fontSize: 17.0)
super.init(selectable: false, topSeparatorInset: .regular)
self.addSubnode(self.countryButton)
self.addSubnode(self.phoneInputNode)
self.phoneInputNode.countryCodeTextUpdated = { [weak self] value in
self?.countryCodeTextUpdated(value)
}
self.phoneInputNode.numberTextUpdated = { [weak self] value in
self?.numberTextUpdated(value)
}
self.countryButton.addTarget(self, action: #selector(self.countryButtonPressed), forControlEvents: .touchUpInside)
}
override func update(item: SecureIdValueFormPhoneItem, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
if self.theme !== theme {
self.countryButton.setBackgroundImage(countryButtonBackground(color: theme.list.itemBlocksBackgroundColor, separatorColor: theme.list.itemBlocksSeparatorColor), for: [])
self.countryButton.setBackgroundImage(countryButtonHighlightedBackground(fillColor: theme.list.itemHighlightedBackgroundColor), for: .highlighted)
self.theme = theme
self.phoneInputNode.countryCodeField.textField.textColor = theme.list.itemPrimaryTextColor
self.phoneInputNode.numberField.textField.textColor = theme.list.itemPrimaryTextColor
self.phoneInputNode.countryCodeField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.numberField.textField.tintColor = theme.list.itemAccentColor
self.phoneInputNode.countryCodeField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.phoneInputNode.numberField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
}
self.item = item
return (FormControllerItemPreLayout(aligningInset: 0.0), { params in
transition.updateFrame(node: self.countryButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: 44.0 + 6.0)))
let buttonTitle: NSAttributedString
if item.countryName.isEmpty {
buttonTitle = NSAttributedString(string: strings.Login_CountryCode, font: Font.regular(17.0), textColor: theme.list.itemSecondaryTextColor)
} else {
buttonTitle = NSAttributedString(string: item.countryName, font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
}
self.countryButton.setAttributedTitle(buttonTitle, for: [])
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: theme.list.itemSecondaryTextColor)
let countryCodeFrame = CGRect(origin: CGPoint(x: 12.0, y: 44.0 + 1.0), size: CGSize(width: 45.0, height: 44.0))
let numberFrame = CGRect(origin: CGPoint(x: 70.0, y: 44.0 + 1.0), size: CGSize(width: width - 70.0 - 8.0, height: 44.0))
let phoneInputFrame = countryCodeFrame.union(numberFrame)
self.phoneInputNode.countryCodeText = item.countryCode
self.phoneInputNode.numberText = item.number
transition.updateFrame(node: self.phoneInputNode, frame: phoneInputFrame)
transition.updateFrame(node: self.phoneInputNode.countryCodeField, frame: countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
transition.updateFrame(node: self.phoneInputNode.numberField, frame: numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
return 88.0
})
}
@objc private func countryButtonPressed() {
self.item?.openCountrySelection()
}
private func countryCodeTextUpdated(_ value: String) {
self.item?.updateCountryCode(value)
}
private func numberTextUpdated(_ value: String) {
self.item?.updateNumber(value)
}
func activate() {
self.phoneInputNode.numberField.becomeFirstResponder()
}
}
@@ -0,0 +1,32 @@
import Foundation
import TelegramCore
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
extension SecureIdDate {
init?(timestamp: Int32) {
let serializedString = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp)))
let data = serializedString.components(separatedBy: ".")
guard data.count == 3 else {
return nil
}
guard let day = Int32(data[0]), let month = Int32(data[1]), let year = Int32(data[2]) else {
return nil
}
self.init(day: day, month: month, year: year)
}
var timestamp: Int32 {
if let date = dateFormatter.date(from: "\(self.day).\(self.month).\(self.year)") {
return Int32(date.timeIntervalSince1970)
} else {
return 0
}
}
}
@@ -0,0 +1,79 @@
import Foundation
import TelegramCore
enum SecureIdVerificationLocalDocumentState: Equatable {
case uploading(Float)
case uploaded(UploadedSecureIdFile)
}
struct SecureIdVerificationLocalDocument: Equatable {
let id: Int64
let resource: TelegramMediaResource
let timestamp: Int32
var state: SecureIdVerificationLocalDocumentState
static func ==(lhs: SecureIdVerificationLocalDocument, rhs: SecureIdVerificationLocalDocument) -> Bool {
if lhs.id != rhs.id {
return false
}
if !lhs.resource.isEqual(to: rhs.resource) {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.state != rhs.state {
return false
}
return true
}
}
enum SecureIdVerificationDocumentId: Hashable {
case remote(Int64)
case local(Int64)
}
enum SecureIdVerificationDocument: Equatable {
case remote(SecureIdFileReference)
case local(SecureIdVerificationLocalDocument)
var id: SecureIdVerificationDocumentId {
switch self {
case let .remote(file):
return .remote(file.id)
case let .local(file):
return .local(file.id)
}
}
var timestamp: Int32 {
switch self {
case let .remote(file):
return file.timestamp
case let .local(file):
return file.timestamp
}
}
var resource: TelegramMediaResource {
switch self {
case let .remote(file):
return SecureFileMediaResource(file: file)
case let .local(file):
return file.resource
}
}
}
extension SecureIdVerificationDocument {
init?(_ reference: SecureIdVerificationDocumentReference) {
switch reference {
case let .remote(file):
self = .remote(file)
case .uploaded:
return nil
}
}
}
@@ -0,0 +1,76 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
private final class DocumentContext {
private let disposable: Disposable
init(disposable: Disposable) {
self.disposable = disposable
}
deinit {
self.disposable.dispose()
}
}
final class SecureIdVerificationDocumentsContext {
private let context: SecureIdAccessContext
private let postbox: Postbox
private let network: Network
private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void
private var contexts: [Int64: DocumentContext] = [:]
private(set) var uploadedFiles: [Data: Data] = [:]
init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) {
self.postbox = postbox
self.network = network
self.context = context
self.update = update
}
func stateUpdated(_ documents: [SecureIdVerificationDocument]) {
var validIds = Set<Int64>()
for document in documents {
switch document {
case let .local(info):
validIds.insert(info.id)
if self.contexts[info.id] == nil {
let disposable = MetaDisposable()
self.contexts[info.id] = DocumentContext(disposable: disposable)
disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
switch result {
case let .progress(value):
if strongSelf.contexts[info.id] != nil {
strongSelf.update(info.id, .uploading(value))
}
case let .result(file, data):
if strongSelf.contexts[info.id] != nil {
strongSelf.uploadedFiles[file.fileHash] = data
strongSelf.update(info.id, .uploaded(file))
}
}
}
}, error: { _ in
}))
}
case .remote:
break
}
}
var removeIds: [Int64] = []
for (id, _) in self.contexts {
if !validIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
self.contexts.removeValue(forKey: id)
}
}
}