Swiftgram/submodules/WalletUI/Sources/WalletConfgurationScreen.swift
2020-03-25 17:59:03 +04:00

352 lines
18 KiB
Swift

import Foundation
import UIKit
import AppBundle
import AsyncDisplayKit
import Display
import SwiftSignalKit
import OverlayStatusController
import WalletCore
private final class WalletConfigurationScreenArguments {
let updateState: ((WalletConfigurationScreenState) -> WalletConfigurationScreenState) -> Void
let dismissInput: () -> Void
let updateSelectedMode: (WalletConfigurationScreenMode) -> Void
let updateBlockchainName: (String) -> Void
init(updateState: @escaping ((WalletConfigurationScreenState) -> WalletConfigurationScreenState) -> Void, dismissInput: @escaping () -> Void, updateSelectedMode: @escaping (WalletConfigurationScreenMode) -> Void, updateBlockchainName: @escaping (String) -> Void) {
self.updateState = updateState
self.dismissInput = dismissInput
self.updateSelectedMode = updateSelectedMode
self.updateBlockchainName = updateBlockchainName
}
}
private enum WalletConfigurationScreenMode {
case url
case customString
}
private enum WalletConfigurationScreenSection: Int32 {
case mode
case configString
case blockchainName
}
private enum WalletConfigurationScreenEntryTag: ItemListItemTag {
case configStringText
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? WalletConfigurationScreenEntryTag {
return self == other
} else {
return false
}
}
}
private enum WalletConfigurationScreenEntry: ItemListNodeEntry, Equatable {
case modeHeader(WalletTheme, String)
case modeUrl(WalletTheme, String, Bool)
case modeCustomString(WalletTheme, String, Bool)
case modeInfo(WalletTheme, String)
case configUrl(WalletTheme, WalletStrings, String, String)
case configString(WalletTheme, String, String)
case blockchainNameHeader(WalletTheme, String)
case blockchainName(WalletTheme, WalletStrings, String, String)
case blockchainNameInfo(WalletTheme, String)
var section: ItemListSectionId {
switch self {
case .modeHeader, .modeUrl, .modeCustomString, .modeInfo:
return WalletConfigurationScreenSection.mode.rawValue
case .configUrl, .configString:
return WalletConfigurationScreenSection.configString.rawValue
case .blockchainNameHeader, .blockchainName, .blockchainNameInfo:
return WalletConfigurationScreenSection.blockchainName.rawValue
}
}
var stableId: Int32 {
switch self {
case .modeHeader:
return 0
case .modeUrl:
return 1
case .modeCustomString:
return 2
case .modeInfo:
return 3
case .configUrl:
return 4
case .configString:
return 5
case .blockchainNameHeader:
return 6
case .blockchainName:
return 7
case .blockchainNameInfo:
return 8
}
}
static func <(lhs: WalletConfigurationScreenEntry, rhs: WalletConfigurationScreenEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! WalletConfigurationScreenArguments
switch self {
case let .modeHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .modeUrl(theme, text, isSelected):
return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSelectedMode(.url)
})
case let .modeCustomString(theme, text, isSelected):
return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSelectedMode(.customString)
})
case let .modeInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .configUrl(theme, _, placeholder, text):
return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .done, minimalHeight: nil, textUpdated: { value in
arguments.updateState { state in
var state = state
state.configUrl = value
return state
}
}, shouldUpdateText: { _ in
return true
}, processPaste: nil, updatedFocus: nil, tag: WalletConfigurationScreenEntryTag.configStringText, action: nil, inlineAction: nil)
case let .configString(theme, placeholder, text):
return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .done, minimalHeight: nil, textUpdated: { value in
arguments.updateState { state in
var state = state
state.configString = value
return state
}
}, shouldUpdateText: { _ in
return true
}, processPaste: nil, updatedFocus: nil, tag: WalletConfigurationScreenEntryTag.configStringText, action: nil, inlineAction: nil)
case let .blockchainNameHeader(theme, title):
return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section)
case let .blockchainName(theme, strings, title, value):
return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: value, placeholder: title, sectionId: self.section, textUpdated: { value in
arguments.updateBlockchainName(value)
}, action: {})
case let .blockchainNameInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
}
}
}
private struct WalletConfigurationScreenState: Equatable {
var mode: WalletConfigurationScreenMode
var configUrl: String
var configString: String
var blockchainName: String
var isEmpty: Bool {
if self.blockchainName.isEmpty {
return true
}
switch self.mode {
case .url:
return self.configUrl.isEmpty || URL(string: self.configUrl) == nil
case .customString:
return self.configString.isEmpty
}
}
}
private func walletConfigurationScreenEntries(presentationData: WalletPresentationData, state: WalletConfigurationScreenState) -> [WalletConfigurationScreenEntry] {
var entries: [WalletConfigurationScreenEntry] = []
entries.append(.modeHeader(presentationData.theme, presentationData.strings.Wallet_Configuration_SourceHeader))
entries.append(.modeUrl(presentationData.theme, presentationData.strings.Wallet_Configuration_SourceURL, state.mode == .url))
entries.append(.modeCustomString(presentationData.theme, presentationData.strings.Wallet_Configuration_SourceJSON, state.mode == .customString))
entries.append(.modeInfo(presentationData.theme, presentationData.strings.Wallet_Configuration_SourceInfo))
switch state.mode {
case .url:
entries.append(.configUrl(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Configuration_SourceURL, state.configUrl))
case .customString:
entries.append(.configString(presentationData.theme, presentationData.strings.Wallet_Configuration_SourceJSON, state.configString))
}
entries.append(.blockchainNameHeader(presentationData.theme, presentationData.strings.Wallet_Configuration_BlockchainIdHeader))
entries.append(.blockchainName(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Configuration_BlockchainIdPlaceholder, state.blockchainName))
entries.append(.blockchainNameInfo(presentationData.theme, presentationData.strings.Wallet_Configuration_BlockchainIdInfo))
return entries
}
protocol WalletConfigurationScreen {
}
private final class WalletConfigurationScreenImpl: ItemListController, WalletConfigurationScreen {
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
return CGSize(width: layout.size.width, height: layout.size.height - 174.0)
}
}
private func presentError(context: WalletContext, present: ((ViewController, Any?) -> Void)?, title: String?, text: String) {
present?(standardTextAlertController(theme: context.presentationData.theme.alert, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: context.presentationData.strings.Wallet_Alert_OK, action: {})]), nil)
}
func walletConfigurationScreen(context: WalletContext, currentConfiguration: LocalWalletConfiguration) -> ViewController {
var configUrl = ""
var configString = ""
switch currentConfiguration.source {
case let .url(url):
configUrl = url
case let .string(string):
configString = string
}
let initialState = WalletConfigurationScreenState(mode: configString.isEmpty ? .url : .customString, configUrl: configUrl, configString: configString, blockchainName: currentConfiguration.blockchainName)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((WalletConfigurationScreenState) -> WalletConfigurationScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
let arguments = WalletConfigurationScreenArguments(updateState: { f in
updateState(f)
}, dismissInput: {
dismissInputImpl?()
}, updateSelectedMode: { mode in
updateState { state in
var state = state
state.mode = mode
return state
}
}, updateBlockchainName: { value in
updateState { state in
var state = state
state.blockchainName = value
return state
}
})
let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Configuration_Apply), style: .bold, enabled: !state.isEmpty, action: {
let state = stateValue.with { $0 }
let source: LocalWalletConfigurationSource
let blockchainName = state.blockchainName
if blockchainName.isEmpty {
return
}
switch state.mode {
case .url:
if state.configUrl.isEmpty {
return
} else {
source = .url(state.configUrl)
}
case .customString:
if state.configString.isEmpty {
return
} else {
source = .string(state.configString)
}
}
if currentConfiguration.source != source || currentConfiguration.blockchainName != blockchainName {
let applyResolved: (String) -> Void = { resolvedConfig in
let proceed: () -> Void = {
let _ = (context.updateResolvedWalletConfiguration(source: source, blockchainName: blockchainName, resolvedValue: resolvedConfig)
|> deliverOnMainQueue).start(completed: {
dismissImpl?()
})
}
if blockchainName != currentConfiguration.blockchainName {
presentControllerImpl?(standardTextAlertController(theme: context.presentationData.theme.alert, title: context.presentationData.strings.Wallet_Configuration_BlockchainNameChangedTitle, text: context.presentationData.strings.Wallet_Configuration_BlockchainNameChangedText, actions: [
TextAlertAction(type: .genericAction, title: context.presentationData.strings.Wallet_Alert_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: context.presentationData.strings.Wallet_Configuration_BlockchainNameChangedProceed, action: {
proceed()
}),
]), nil)
} else {
proceed()
}
}
let presentationData = context.presentationData
switch source {
case let .url(url):
if let parsedUrl = URL(string: url) {
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(statusController, nil)
let _ = (context.downloadFile(url: parsedUrl)
|> deliverOnMainQueue).start(next: { data in
statusController.dismiss()
guard let string = String(data: data, encoding: .utf8) else {
let presentationData = context.presentationData
presentError(context: context, present: presentControllerImpl, title: presentationData.strings.Wallet_Configuration_ApplyErrorTitle, text: presentationData.strings.Wallet_Configuration_ApplyErrorTextURLInvalidData)
return
}
let _ = (context.tonInstance.validateConfig(config: string, blockchainName: blockchainName)
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.presentationData
presentError(context: context, present: presentControllerImpl, title: presentationData.strings.Wallet_Configuration_ApplyErrorTitle, text: presentationData.strings.Wallet_Configuration_ApplyErrorTextURLInvalidData)
}, completed: {
applyResolved(string)
})
}, error: { _ in
statusController.dismiss()
let presentationData = context.presentationData
presentError(context: context, present: presentControllerImpl, title: presentationData.strings.Wallet_Configuration_ApplyErrorTitle, text: presentationData.strings.Wallet_Configuration_ApplyErrorTextURLUnreachable(url).0)
})
} else {
presentError(context: context, present: presentControllerImpl, title: presentationData.strings.Wallet_Configuration_ApplyErrorTitle, text: presentationData.strings.Wallet_Configuration_ApplyErrorTextURLInvalid)
return
}
case let .string(string):
let _ = (context.tonInstance.validateConfig(config: string, blockchainName: blockchainName)
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.presentationData
presentError(context: context, present: presentControllerImpl, title: presentationData.strings.Wallet_Configuration_ApplyErrorTitle, text: presentationData.strings.Wallet_Configuration_ApplyErrorTextJSONInvalidData)
}, completed: {
applyResolved(string)
})
}
} else {
dismissImpl?()
}
})
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_Configuration_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletConfigurationScreenEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = WalletConfigurationScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.experimentalSnapScrollToItem = true
controller.didAppear = { _ in
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
return controller
}