import Foundation import UIKit import AppBundle import AccountContext import TelegramPresentationData import AsyncDisplayKit import Display import Postbox import TelegramCore import ItemListUI import SwiftSignalKit import AlertUI import TextFormat import DeviceAccess import TelegramStringFormatting import UrlHandling import OverlayStatusController private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed() private final class WalletSendScreenArguments { let context: AccountContext let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void let updateText: (WalletSendScreenEntryTag, String) -> Void let selectNextInputItem: (WalletSendScreenEntryTag) -> Void let scrollToBottom: () -> Void let dismissInput: () -> Void let openQrScanner: () -> Void let proceed: () -> Void init(context: AccountContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, updateText: @escaping (WalletSendScreenEntryTag, String) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, scrollToBottom: @escaping () -> Void, dismissInput: @escaping () -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) { self.context = context self.updateState = updateState self.updateText = updateText self.selectNextInputItem = selectNextInputItem self.scrollToBottom = scrollToBottom self.dismissInput = dismissInput self.openQrScanner = openQrScanner self.proceed = proceed } } private enum WalletSendScreenSection: Int32 { case address case amount case comment } private enum WalletSendScreenEntryTag: ItemListItemTag { case address case amount case comment func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? WalletSendScreenEntryTag { return self == other } else { return false } } } private enum WalletSendScreenEntry: ItemListNodeEntry { case addressHeader(PresentationTheme, String) case address(PresentationTheme, String, String) case addressInfo(PresentationTheme, String) case amountHeader(PresentationTheme, String, String?, Bool) case amount(PresentationTheme, PresentationStrings, String, String) case commentHeader(PresentationTheme, String) case comment(PresentationTheme, String, String) var section: ItemListSectionId { switch self { case .addressHeader, .address, .addressInfo: return WalletSendScreenSection.address.rawValue case .amountHeader, .amount: return WalletSendScreenSection.amount.rawValue case .commentHeader, .comment: return WalletSendScreenSection.comment.rawValue } } var stableId: Int32 { switch self { case .addressHeader: return 0 case .address: return 1 case .addressInfo: return 2 case .amountHeader: return 3 case .amount: return 4 case .commentHeader: return 5 case .comment: return 6 } } static func ==(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool { switch lhs { case let .addressHeader(lhsTheme, lhsText): if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .address(lhsTheme, lhsPlaceholder, lhsAddress): if case let .address(rhsTheme, rhsPlaceholder, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsAddress == rhsAddress { return true } else { return false } case let .addressInfo(lhsTheme, lhsText): if case let .addressInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .amountHeader(lhsTheme, lhsText, lhsBalance, lhsInsufficient): if case let .amountHeader(rhsTheme, rhsText, rhsBalance, rhsInsufficient) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsBalance == rhsBalance, lhsInsufficient == rhsInsufficient { return true } else { return false } case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsAmount): if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsAmount == rhsAmount { return true } else { return false } case let .commentHeader(lhsTheme, lhsText): if case let .commentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .comment(lhsTheme, lhsPlaceholder, lhsText): if case let .comment(rhsTheme, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText { return true } else { return false } } } static func <(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(_ arguments: WalletSendScreenArguments) -> ListViewItem { switch self { case let .addressHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .address(theme, placeholder, address): return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { text in arguments.updateText(WalletSendScreenEntryTag.address, text.replacingOccurrences(of: "\n", with: "")) }, shouldUpdateText: { text in return isValidAddress(text) }, processPaste: { text in if let url = URL(string: text), let parsedUrl = parseWalletUrl(url) { var focusItemTag: WalletSendScreenEntryTag? arguments.updateState { state in var state = state state.address = parsedUrl.address if let amount = parsedUrl.amount { state.amount = formatBalanceText(amount, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator) } else if state.amount.isEmpty { focusItemTag = WalletSendScreenEntryTag.address } if let comment = parsedUrl.comment { state.comment = comment } else if state.comment.isEmpty && focusItemTag == nil { focusItemTag = WalletSendScreenEntryTag.amount } return state } if let focusItemTag = focusItemTag { arguments.selectNextInputItem(focusItemTag) } else { arguments.dismissInput() } } else if isValidAddress(text) { arguments.updateText(WalletSendScreenEntryTag.address, text) if isValidAddress(text, exactLength: true) { arguments.selectNextInputItem(WalletSendScreenEntryTag.address) } } }, tag: WalletSendScreenEntryTag.address, action: { arguments.selectNextInputItem(WalletSendScreenEntryTag.address) }, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: { arguments.openQrScanner() })) case let .addressInfo(theme, text): return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) case let .amountHeader(theme, text, balance, insufficient): return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section) case let .amount(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, clearType: .onFocus, tag: WalletSendScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in let text = formatAmountText(text, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator) arguments.updateText(WalletSendScreenEntryTag.amount, text) }, shouldUpdateText: { text in return isValidAmount(text) }, processPaste: { pastedText in if isValidAmount(pastedText) { let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 } return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } else { return text } }, updatedFocus: { focus in if !focus { let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 } arguments.updateState { state in var state = state if !state.amount.isEmpty { state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } return state } } }, action: { arguments.selectNextInputItem(WalletSendScreenEntryTag.amount) }) case let .commentHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .comment(theme, placeholder, value): return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: walletTextLimit, display: true, mode: .bytes), sectionId: self.section, style: .blocks, returnKeyType: .send, textUpdated: { text in arguments.updateText(WalletSendScreenEntryTag.comment, text) }, updatedFocus: { focus in if focus { arguments.scrollToBottom() } }, tag: WalletSendScreenEntryTag.comment, action: { arguments.proceed() }) } } } private struct WalletSendScreenState: Equatable { var address: String var amount: String var comment: String } private func walletSendScreenEntries(presentationData: PresentationData, balance: Int64?, state: WalletSendScreenState) -> [WalletSendScreenEntry] { if balance == nil { return [] } var entries: [WalletSendScreenEntry] = [] entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Send_AddressHeader)) entries.append(.address(presentationData.theme, presentationData.strings.Wallet_Send_AddressText, state.address)) entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Send_AddressInfo)) let amount = amountValue(state.amount) let balance = max(0, balance ?? 0) entries.append(.amountHeader(presentationData.theme, presentationData.strings.Wallet_Receive_AmountHeader, presentationData.strings.Wallet_Send_Balance(formatBalanceText(balance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)).0, balance == 0 || (amount > 0 && balance < amount))) entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Send_AmountText, state.amount ?? "")) entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_Receive_CommentHeader)) entries.append(.comment(presentationData.theme, presentationData.strings.Wallet_Receive_CommentInfo, state.comment)) return entries } protocol WalletSendScreen { } private final class WalletSendScreenImpl: ItemListController, WalletSendScreen { } public func walletSendScreen(context: AccountContext, tonContext: TonContext, randomId: Int64, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil, comment: String? = nil) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: comment ?? "") let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } let serverSaltValue = Promise() serverSaltValue.set(getServerWalletSalt(network: context.account.network) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) }) var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)? var pushImpl: ((ViewController) -> Void)? var popImpl: (() -> Void)? var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)? var ensureItemVisibleImpl: ((WalletSendScreenEntryTag, Bool) -> Void)? let arguments = WalletSendScreenArguments(context: context, updateState: { f in updateState(f) }, updateText: { tag, value in updateState { state in var state = state switch tag { case .address: state.address = value case .amount: state.amount = value case .comment: state.comment = value } return state } ensureItemVisibleImpl?(tag, false) }, selectNextInputItem: { tag in selectNextInputItemImpl?(tag) }, scrollToBottom: { ensureItemVisibleImpl?(WalletSendScreenEntryTag.comment, true) }, dismissInput: { dismissInputImpl?() }, openQrScanner: { dismissInputImpl?() DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in presentControllerImpl?(c, a) }, openSettings: { context.sharedContext.applicationBindings.openSettings() }, { granted in guard granted else { return } pushImpl?(WalletQrScanScreen(context: context, completion: { parsedUrl in var updatedState: WalletSendScreenState? updateState { state in var state = state state.address = parsedUrl.address if let amount = parsedUrl.amount { state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } if let comment = parsedUrl.comment { state.comment = comment } updatedState = state return state } popImpl?() if let updatedState = updatedState { if updatedState.amount.isEmpty { selectNextInputItemImpl?(WalletSendScreenEntryTag.address) } else if updatedState.comment.isEmpty { selectNextInputItemImpl?(WalletSendScreenEntryTag.amount) } } })) }) }, proceed: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let state = stateValue.with { $0 } let amount = amountValue(state.amount) updateState { state in var state = state state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) return state } let title = NSAttributedString(string: presentationData.strings.Wallet_Send_Confirmation, font: Font.semibold(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) let address = state.address[state.address.startIndex.. Void)? let controller = richTextAlertController(context: context, title: title, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissAlertImpl?(true) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Send_ConfirmationConfirm, action: { dismissAlertImpl?(false) dismissInputImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: nil)) presentControllerImpl?(controller, nil) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() var serverSaltSignal = serverSaltValue.get() |> take(1) serverSaltSignal = serverSaltSignal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } let _ = (serverSaltSignal |> deliverOnMainQueue).start(next: { serverSalt in if let serverSalt = serverSalt { if let commentData = state.comment.data(using: .utf8) { pushImpl?(WalletSplashScreen(context: context, tonContext: tonContext, mode: .sending(walletInfo, state.address, amount, commentData, randomId, serverSalt), walletCreatedPreloadState: nil)) } } }) })], allowInputInset: false, dismissAutomatically: false) presentInGlobalOverlayImpl?(controller, nil) dismissAlertImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() } else { controller?.dismiss() } } }) let walletState: Signal = getCombinedWalletState(postbox: context.account.postbox, subject: .wallet(walletInfo), tonInstance: tonContext.instance, onlyCached: true) |> map { combinedState in var state: WalletState? switch combinedState { case let .cached(combinedState): state = combinedState?.walletState case let .updated(combinedState): state = combinedState.walletState } return state } |> `catch` { _ -> Signal in return .single(nil) |> then( getCombinedWalletState(postbox: context.account.postbox, subject: .wallet(walletInfo), tonInstance: tonContext.instance, onlyCached: false) |> map { combinedState -> WalletState? in var state: WalletState? switch combinedState { case let .cached(combinedState): state = combinedState?.walletState case let .updated(combinedState): state = combinedState.walletState } return state } |> `catch` { _ -> Signal in return .single(nil) } ) } var focusItemTag: ItemListItemTag? if address == nil { focusItemTag = WalletSendScreenEntryTag.address } else if amount == nil { focusItemTag = WalletSendScreenEntryTag.amount } let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, walletState, statePromise.get()) |> map { presentationData, walletState, state -> (ItemListControllerState, (ItemListNodeState, WalletSendScreenEntry.ItemGenerationArguments)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let rightNavigationButton: ItemListNavigationButton? let amount = amountValue(state.amount) var sendEnabled = false var emptyItem: ItemListControllerEmptyStateItem? if let walletState = walletState { let textLength: Int = state.comment.data(using: .utf8, allowLossyConversion: true)?.count ?? 0 sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= walletState.balance && textLength <= walletTextLimit rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Send_Send), style: .bold, enabled: sendEnabled, action: { arguments.proceed() }) } else { rightNavigationButton = nil emptyItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_Send_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: walletSendScreenEntries(presentationData: presentationData, balance: walletState?.balance, state: state), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: emptyItem, animateChanges: false) return (controllerState, (listState, arguments)) } let controller = WalletSendScreenImpl(context: context, state: signal) controller.navigationPresentation = .modal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } presentInGlobalOverlayImpl = { [weak controller] c, a in controller?.presentInGlobalOverlay(c, with: a) } pushImpl = { [weak controller] c in controller?.push(c) } popImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popViewController(animated: true) } dismissImpl = { [weak controller] in controller?.view.endEditing(true) let _ = controller?.dismiss() } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } selectNextInputItemImpl = { [weak controller] currentTag in guard let controller = controller else { return } var resultItemNode: ItemListItemFocusableNode? var focusOnNext = false let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, let focusableItemNode = itemNode as? ItemListItemFocusableNode { if focusOnNext && resultItemNode == nil { resultItemNode = focusableItemNode return true } else if currentTag.isEqual(to: tag) { focusOnNext = true } } return false }) if let resultItemNode = resultItemNode { resultItemNode.focus() } } ensureItemVisibleImpl = { [weak controller] targetTag, animated in controller?.afterLayout({ guard let controller = controller else { return } var resultItemNode: ListViewItemNode? let state = stateValue.with({ $0 }) let _ = controller.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListItemNode { if let tag = itemNode.tag, tag.isEqual(to: targetTag) { resultItemNode = itemNode as? ListViewItemNode return true } } return false }) if let resultItemNode = resultItemNode { controller.ensureItemNodeVisible(resultItemNode, animated: animated) } }) } return controller }