import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SyncCore import SwiftSignalKit import PassKit import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils import TelegramNotices import TelegramStringFormatting import PasswordSetupUI import Stripe import LocalAuth import OverlayStatusController final class BotCheckoutControllerArguments { fileprivate let account: Account fileprivate let openInfo: (BotCheckoutInfoControllerFocus) -> Void fileprivate let openPaymentMethod: () -> Void fileprivate let openShippingMethod: () -> Void fileprivate let updateTip: (Int64) -> Void fileprivate let ensureTipInputVisible: () -> Void fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, updateTip: @escaping (Int64) -> Void, ensureTipInputVisible: @escaping () -> Void) { self.account = account self.openInfo = openInfo self.openPaymentMethod = openPaymentMethod self.openShippingMethod = openShippingMethod self.updateTip = updateTip self.ensureTipInputVisible = ensureTipInputVisible } } private enum BotCheckoutSection: Int32 { case header case prices case info } enum BotCheckoutEntry: ItemListNodeEntry { enum StableId: Hashable { case header case price(Int) case actionPlaceholder(Int) case tip case paymentMethod case shippingInfo case shippingMethod case nameInfo case emailInfo case phoneInfo } case header(PresentationTheme, TelegramMediaInvoice, String) case price(Int, PresentationTheme, String, String, Bool, Bool, Int?) case tip(Int, PresentationTheme, String, String, String, Int64, Int64, [(String, Int64)]) case paymentMethod(PresentationTheme, String, String) case shippingInfo(PresentationTheme, String, String) case shippingMethod(PresentationTheme, String, String) case nameInfo(PresentationTheme, String, String) case emailInfo(PresentationTheme, String, String) case phoneInfo(PresentationTheme, String, String) case actionPlaceholder(Int, Int) var section: ItemListSectionId { switch self { case .header: return BotCheckoutSection.prices.rawValue case .price, .tip: return BotCheckoutSection.prices.rawValue default: return BotCheckoutSection.info.rawValue } } var sortId: Int32 { switch self { case .header: return 0 case let .price(index, _, _, _, _, _, _): return 1 + Int32(index) case let .tip(index, _, _, _, _, _, _, _): return 1 + Int32(index) case let .actionPlaceholder(index, _): return 1 + Int32(index) case .paymentMethod: return 10000 + 2 case .shippingInfo: return 10000 + 3 case .shippingMethod: return 10000 + 4 case .nameInfo: return 10000 + 5 case .emailInfo: return 10000 + 6 case .phoneInfo: return 10000 + 7 } } var stableId: StableId { switch self { case .header: return .header case let .price(index, _, _, _, _, _, _): return .price(index) case .tip: return .tip case let .actionPlaceholder(index, _): return .actionPlaceholder(index) case .paymentMethod: return .paymentMethod case .shippingInfo: return .shippingInfo case .shippingMethod: return .shippingMethod case .nameInfo: return .nameInfo case .emailInfo: return .emailInfo case .phoneInfo: return .phoneInfo } } static func ==(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { switch lhs { case let .header(lhsTheme, lhsInvoice, lhsName): if case let .header(rhsTheme, rhsInvoice, rhsName) = rhs { if lhsTheme !== rhsTheme { return false } if !lhsInvoice.isEqual(to: rhsInvoice) { return false } if lhsName != rhsName { return false } return true } else { return false } case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator, lhsShimmeringIndex): if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator, rhsShimmeringIndex) = rhs { if lhsIndex != rhsIndex { return false } if lhsTheme !== rhsTheme { return false } if lhsText != rhsText { return false } if lhsValue != rhsValue { return false } if lhsFinal != rhsFinal { return false } if lhsHasSeparator != rhsHasSeparator { return false } if lhsShimmeringIndex != rhsShimmeringIndex { return false } return true } else { return false } case let .tip(lhsIndex, lhsTheme, lhsText, lhsCurrency, lhsValue, lhsNumericValue, lhsMaxValue, lhsVariants): if case let .tip(rhsIndex, rhsTheme, rhsText, rhsCurrency, rhsValue, rhsNumericValue, rhsMaxValue, rhsVariants) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsCurrency == rhsCurrency, lhsValue == rhsValue, lhsNumericValue == rhsNumericValue, lhsMaxValue == rhsMaxValue { if lhsVariants.count != rhsVariants.count { return false } for i in 0 ..< lhsVariants.count { if lhsVariants[i].0 != rhsVariants[i].0 { return false } if lhsVariants[i].1 != rhsVariants[i].1 { return false } } return true } else { return false } case let .paymentMethod(lhsTheme, lhsText, lhsValue): if case let .paymentMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .shippingInfo(lhsTheme, lhsText, lhsValue): if case let .shippingInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .shippingMethod(lhsTheme, lhsText, lhsValue): if case let .shippingMethod(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .nameInfo(lhsTheme, lhsText, lhsValue): if case let .nameInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .emailInfo(lhsTheme, lhsText, lhsValue): if case let .emailInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .phoneInfo(lhsTheme, lhsText, lhsValue): if case let .phoneInfo(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .actionPlaceholder(index, shimmeringIndex): if case .actionPlaceholder(index, shimmeringIndex) = rhs { return true } else { return false } } } static func <(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! BotCheckoutControllerArguments switch self { case let .header(theme, invoice, botName): return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) case let .price(_, theme, text, value, isFinal, hasSeparator, shimmeringIndex): return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: shimmeringIndex, sectionId: self.section) case let .tip(_, _, text, currency, value, numericValue, maxValue, variants): return BotCheckoutTipItem(theme: presentationData.theme, strings: presentationData.strings, title: text, currency: currency, value: value, numericValue: numericValue, maxValue: maxValue, availableVariants: variants, sectionId: self.section, updateValue: { value in arguments.updateTip(value) }, updatedFocus: { isFocused in if isFocused { arguments.ensureTipInputVisible() } }) case let .paymentMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPaymentMethod() }) case let .shippingInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.address(.street1)) }) case let .shippingMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openShippingMethod() }) case let .nameInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.name) }) case let .emailInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.email) }) case let .phoneInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.phone) }) case let .actionPlaceholder(_, shimmeringIndex): return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { }, shimmeringIndex: shimmeringIndex) } } } private struct BotCheckoutControllerState: Equatable { init() { } static func ==(lhs: BotCheckoutControllerState, rhs: BotCheckoutControllerState) -> Bool { return true } } private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentTip: Int64?) -> Int64 { guard let paymentForm = paymentForm else { return 0 } var totalPrice: Int64 = 0 if let currentTip = currentTip { totalPrice += currentTip } var index = 0 for price in paymentForm.invoice.prices { totalPrice += price.amount index += 1 } if let validatedFormInfo = validatedFormInfo, let shippingOptions = validatedFormInfo.shippingOptions { if let currentShippingOptionId = currentShippingOptionId { for option in shippingOptions { if option.id == currentShippingOptionId { for price in option.prices { totalPrice += price.amount } break } } } } return totalPrice } private func botCheckoutControllerEntries(presentationData: PresentationData, state: BotCheckoutControllerState, invoice: TelegramMediaInvoice, paymentForm: BotPaymentForm?, formInfo: BotPaymentRequestedInfo?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentPaymentMethod: BotCheckoutPaymentMethod?, currentTip: Int64?, botPeer: Peer?) -> [BotCheckoutEntry] { var entries: [BotCheckoutEntry] = [] var botName = "" if let botPeer = botPeer { botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } entries.append(.header(presentationData.theme, invoice, botName)) if let paymentForm = paymentForm { var totalPrice: Int64 = 0 if let currentTip = currentTip { totalPrice += currentTip } var index = 0 for price in paymentForm.invoice.prices { entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0, nil)) totalPrice += price.amount index += 1 } var shippingOptionString: String? if let validatedFormInfo = validatedFormInfo, let shippingOptions = validatedFormInfo.shippingOptions { shippingOptionString = "" if let currentShippingOptionId = currentShippingOptionId { for option in shippingOptions { if option.id == currentShippingOptionId { shippingOptionString = option.title for price in option.prices { entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false, nil)) totalPrice += price.amount index += 1 } break } } } } if !entries.isEmpty { switch entries[entries.count - 1] { case let .price(index, theme, title, value, _, _, _): entries[entries.count - 1] = .price(index, theme, title, value, false, index == 0, nil) default: break } } if let tip = paymentForm.invoice.tip { let tipTitle: String tipTitle = presentationData.strings.Checkout_OptionalTipItem entries.append(.tip(index, presentationData.theme, tipTitle, paymentForm.invoice.currency, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))", currentTip ?? 0, tip.max, tip.suggested.map { item -> (String, Int64) in return ("\(formatCurrencyAmount(item, currency: paymentForm.invoice.currency))", item) })) index += 1 } entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true, true, nil)) var paymentMethodTitle = "" if let currentPaymentMethod = currentPaymentMethod { paymentMethodTitle = currentPaymentMethod.title } entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle)) if paymentForm.invoice.requestedFields.contains(.shippingAddress) { var addressString = "" if let address = formInfo?.shippingAddress { let components: [String] = [ address.city, address.streetLine1, address.streetLine2, address.state ] for component in components { if !component.isEmpty { if !addressString.isEmpty { addressString.append(", ") } addressString.append(component) } } } entries.append(.shippingInfo(presentationData.theme, presentationData.strings.Checkout_ShippingAddress, addressString)) if let shippingOptionString = shippingOptionString { entries.append(.shippingMethod(presentationData.theme, presentationData.strings.Checkout_ShippingMethod, shippingOptionString)) } } if paymentForm.invoice.requestedFields.contains(.name) { entries.append(.nameInfo(presentationData.theme, presentationData.strings.Checkout_Name, formInfo?.name ?? "")) } if paymentForm.invoice.requestedFields.contains(.email) { entries.append(.emailInfo(presentationData.theme, presentationData.strings.Checkout_Email, formInfo?.email ?? "")) } if paymentForm.invoice.requestedFields.contains(.phone) { entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? "")) } } else { let numItems = 4 for index in 0 ..< numItems { entries.append(.price(index, presentationData.theme, " ", " ", false, index == 0, index)) } for index in numItems ..< numItems + 2 { entries.append(.actionPlaceholder(index, index - numItems)) } } return entries } private let hasApplePaySupport: Bool = PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: [.visa, .masterCard, .amex]) private func formSupportApplePay(_ paymentForm: BotPaymentForm) -> Bool { if !hasApplePaySupport { return false } guard let nativeProvider = paymentForm.nativeProvider else { return false } let applePayProviders = Set([ "stripe", "sberbank", "yandex", "privatbank", "tranzzo", "paymaster" ]) if !applePayProviders.contains(nativeProvider.name) { return false } guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else { return false } guard let nativeParams = (try? JSONSerialization.jsonObject(with: nativeParamsData, options: [])) as? [String: Any] else { return false } var merchantId: String? if nativeProvider.name == "stripe" { merchantId = "merchant.ph.telegra.Telegraph" } else if let paramsId = nativeParams["apple_pay_merchant_id"] as? String { merchantId = paramsId } return merchantId != nil } private func availablePaymentMethods(form: BotPaymentForm, current: BotCheckoutPaymentMethod?) -> [BotCheckoutPaymentMethod] { var methods: [BotCheckoutPaymentMethod] = [] if formSupportApplePay(form) && hasApplePaySupport { methods.append(.applePay) } if let current = current { if !methods.contains(current) { methods.append(current) } } if let savedCredentials = form.savedCredentials { if !methods.contains(.savedCredentials(savedCredentials)) { methods.append(.savedCredentials(savedCredentials)) } } return methods } final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate { private weak var controller: BotCheckoutController? private let context: AccountContext private let messageId: MessageId private let present: (ViewController, Any?) -> Void private let dismissAnimated: () -> Void private let completed: (String, MessageId?) -> Void private var stateValue = BotCheckoutControllerState() private let state = ValuePromise(BotCheckoutControllerState(), ignoreRepeated: true) private var arguments: BotCheckoutControllerArguments? private var presentationData: PresentationData private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?, Int64?)?>(nil) private var paymentFormValue: BotPaymentForm? private var currentFormInfo: BotPaymentRequestedInfo? private var currentValidatedFormInfo: BotPaymentValidatedFormInfo? private var currentShippingOptionId: String? private var currentPaymentMethod: BotCheckoutPaymentMethod? private var currentTipAmount: Int64? private var formRequestDisposable: Disposable? private let actionButtonPanelNode: ASDisplayNode private let actionButtonPanelSeparator: ASDisplayNode private let actionButton: BotCheckoutActionButton private let inProgressDimNode: ASDisplayNode private var statusController: ViewController? private let payDisposable = MetaDisposable() private let paymentAuthDisposable = MetaDisposable() private var applePayAuthrorizationCompletion: ((PKPaymentAuthorizationStatus) -> Void)? private var applePayController: PKPaymentAuthorizationViewController? private var passwordTip: String? private var passwordTipDisposable: Disposable? init(controller: BotCheckoutController?, navigationBar: NavigationBar, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void, completed: @escaping (String, MessageId?) -> Void) { self.controller = controller self.context = context self.messageId = messageId self.present = present self.dismissAnimated = dismissAnimated self.completed = completed self.presentationData = context.sharedContext.currentPresentationData.with { $0 } var openInfoImpl: ((BotCheckoutInfoControllerFocus) -> Void)? var updateTipImpl: ((Int64) -> Void)? var openPaymentMethodImpl: (() -> Void)? var openShippingMethodImpl: (() -> Void)? var ensureTipInputVisibleImpl: (() -> Void)? let arguments = BotCheckoutControllerArguments(account: context.account, openInfo: { item in openInfoImpl?(item) }, openPaymentMethod: { openPaymentMethodImpl?() }, openShippingMethod: { openShippingMethodImpl?() }, updateTip: { value in updateTipImpl?(value) }, ensureTipInputVisible: { ensureTipInputVisibleImpl?() }) let paymentBotPeer = paymentFormAndInfo.get() |> map { paymentFormAndInfo -> PeerId? in return paymentFormAndInfo?.0.paymentBotId } |> distinctUntilChanged |> mapToSignal { peerId -> Signal in return context.account.postbox.transaction { transaction -> Peer? in return peerId.flatMap(transaction.getPeer) } } let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), paymentBotPeer) |> map { presentationData, state, paymentFormAndInfo, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) return (ItemListPresentationData(presentationData), (nodeState, arguments)) } self.actionButtonPanelNode = ASDisplayNode() self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.actionButtonPanelSeparator = ASDisplayNode() self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) self.actionButton.setState(.placeholder) self.inProgressDimNode = ASDisplayNode() self.inProgressDimNode.alpha = 0.0 self.inProgressDimNode.isUserInteractionEnabled = false self.inProgressDimNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.5) super.init(controller: nil, navigationBar: navigationBar, state: signal) self.arguments = arguments openInfoImpl = { [weak self] focus in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.controller?.view.endEditing(true) strongSelf.present(BotCheckoutInfoController(context: context, invoice: paymentFormValue.invoice, messageId: messageId, initialFormInfo: currentFormInfo, focus: focus, formInfoUpdated: { formInfo, validatedInfo in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue { strongSelf.currentFormInfo = formInfo strongSelf.currentValidatedFormInfo = validatedInfo var updatedCurrentShippingOptionId: String? if let currentShippingOptionId = strongSelf.currentShippingOptionId, let shippingOptions = validatedInfo.shippingOptions { if shippingOptions.contains(where: { $0.id == currentShippingOptionId }) { updatedCurrentShippingOptionId = currentShippingOptionId } } strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, formInfo, validatedInfo, updatedCurrentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } let applyPaymentMethod: (BotCheckoutPaymentMethod) -> Void = { [weak self] method in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentPaymentMethod = method strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } } let openNewCard: () -> Void = { [weak self] in if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" { guard let paramsData = nativeProvider.params.data(using: .utf8) else { return } guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { return } guard let publishableKey = nativeParams["publishable_key"] as? String else { return } var additionalFields: BotCheckoutNativeCardEntryAdditionalFields = [] if let needCardholderName = nativeParams["need_cardholder_name"] as? NSNumber, needCardholderName.boolValue { additionalFields.insert(.cardholderName) } if let needCountry = nativeParams["need_country"] as? NSNumber, needCountry.boolValue { additionalFields.insert(.country) } if let needZip = nativeParams["need_zip"] as? NSNumber, needZip.boolValue { additionalFields.insert(.zipCode) } var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .stripe(additionalFields: additionalFields, publishableKey: publishableKey), completion: { method in guard let strongSelf = self else { return } if canSave && paymentForm.passwordMissing { switch method { case let .webToken(webToken) where webToken.saveOnServer: var text = strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoEnableHelp text = text.replacingOccurrences(of: "[", with: "") text = text.replacingOccurrences(of: "]", with: "") present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_NotNow, action: { var updatedToken = webToken updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { guard let strongSelf = self else { return } if paymentForm.passwordMissing { var updatedToken = webToken updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) let controller = SetupTwoStepVerificationController(context: strongSelf.context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in if shouldDismiss { controller.dismiss() } switch update { case .noPassword, .awaitingEmailConfirmation: break case .passwordSet: var updatedToken = webToken updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } }) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { var updatedToken = webToken updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } })]), nil) default: applyPaymentMethod(method) } } else { applyPaymentMethod(method) } dismissImpl?() }) dismissImpl = { [weak controller] in controller?.dismiss() } strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "smartglocal" { guard let paramsData = nativeProvider.params.data(using: .utf8) else { return } guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { return } guard let publicToken = nativeParams["public_token"] as? String else { return } var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken), completion: { method in guard let strongSelf = self else { return } if canSave && paymentForm.passwordMissing { switch method { case let .webToken(webToken) where webToken.saveOnServer: var text = strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoEnableHelp text = text.replacingOccurrences(of: "[", with: "") text = text.replacingOccurrences(of: "]", with: "") present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_NotNow, action: { var updatedToken = webToken updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { guard let strongSelf = self else { return } if paymentForm.passwordMissing { var updatedToken = webToken updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) let controller = SetupTwoStepVerificationController(context: strongSelf.context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in if shouldDismiss { controller.dismiss() } switch update { case .noPassword, .awaitingEmailConfirmation: break case .passwordSet: var updatedToken = webToken updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } }) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { var updatedToken = webToken updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } })]), nil) default: applyPaymentMethod(method) } } else { applyPaymentMethod(method) } dismissImpl?() }) dismissImpl = { [weak controller] in controller?.dismiss() } strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { var dismissImpl: (() -> Void)? let controller = BotCheckoutWebInteractionController(context: context, url: paymentForm.url, intent: .addPaymentMethod({ [weak self] token in dismissImpl?() guard let strongSelf = self else { return } let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing let allowSaving = paymentForm.canSaveCredentials && !paymentForm.passwordMissing if canSave { present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoHelp, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_NotNow, action: { var updatedToken = token updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { guard let strongSelf = self else { return } if paymentForm.passwordMissing { var updatedToken = token updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) let controller = SetupTwoStepVerificationController(context: strongSelf.context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in if shouldDismiss { controller.dismiss() } switch update { case .noPassword, .awaitingEmailConfirmation: break case .passwordSet: var updatedToken = token updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } }) strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { var updatedToken = token updatedToken.saveOnServer = true applyPaymentMethod(.webToken(updatedToken)) } })]), nil) } else { var updatedToken = token updatedToken.saveOnServer = false applyPaymentMethod(.webToken(updatedToken)) if allowSaving { present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoEnableHelp.replacingOccurrences(of: "]", with: "").replacingOccurrences(of: "[", with: ""), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { })]), nil) } } })) dismissImpl = { [weak controller] in controller?.dismiss() } strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } updateTipImpl = { [weak self] value in guard let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo else { return } if strongSelf.currentTipAmount == value { return } strongSelf.currentTipAmount = value strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } ensureTipInputVisibleImpl = { [weak self] in self?.afterLayout({ guard let strongSelf = self else { return } var selectedItemNode: ListViewItemNode? strongSelf.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? BotCheckoutTipItemNode { selectedItemNode = itemNode } } if let selectedItemNode = selectedItemNode { strongSelf.listNode.ensureItemNodeVisible(selectedItemNode, atTop: true) } }) } openPaymentMethodImpl = { [weak self] in if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { strongSelf.controller?.view.endEditing(true) let methods = availablePaymentMethods(form: paymentForm, current: strongSelf.currentPaymentMethod) if methods.isEmpty { openNewCard() } else { strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in applyPaymentMethod(method) }, newCard: { openNewCard() }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } openShippingMethodImpl = { [weak self] in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty { strongSelf.controller?.view.endEditing(true) strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentShippingOptionId = id strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } self.formRequestDisposable = (inputData.get() |> deliverOnMainQueue).start(next: { [weak self] formAndValidatedInfo in if let strongSelf = self { guard let formAndValidatedInfo = formAndValidatedInfo else { strongSelf.controller?.dismiss() return } UIView.transition(with: strongSelf.view, duration: 0.25, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { }, completion: nil) let savedInfo: BotPaymentRequestedInfo if let current = formAndValidatedInfo.form.savedInfo { savedInfo = current } else { savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil) } strongSelf.paymentFormValue = formAndValidatedInfo.form strongSelf.currentFormInfo = savedInfo strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo if let savedCredentials = formAndValidatedInfo.form.savedCredentials { strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials) } strongSelf.actionButton.isEnabled = true strongSelf.paymentFormAndInfo.set(.single((formAndValidatedInfo.form, savedInfo, formAndValidatedInfo.validatedFormInfo, nil, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } }) self.addSubnode(self.actionButtonPanelNode) self.actionButtonPanelNode.addSubnode(self.actionButtonPanelSeparator) self.actionButtonPanelNode.addSubnode(self.actionButton) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) self.actionButton.isEnabled = false self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode) self.passwordTipDisposable = (twoStepVerificationConfiguration(account: self.context.account) |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } switch value { case .notSet: break case let .set(hint, _, _, _, _): if !hint.isEmpty { strongSelf.passwordTip = hint } } }) } deinit { self.formRequestDisposable?.dispose() self.payDisposable.dispose() self.paymentAuthDisposable.dispose() self.passwordTipDisposable?.dispose() } private func updateActionButton() { let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) let payString: String if let paymentForm = self.paymentFormValue, totalAmount > 0 { payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).0 } else { payString = self.presentationData.strings.CheckoutInfo_Pay } if let currentPaymentMethod = self.currentPaymentMethod { switch currentPaymentMethod { case .applePay: self.actionButton.setState(.applePay) default: self.actionButton.setState(.active(payString)) } } else { self.actionButton.setState(.active(payString)) } self.actionButtonPanelNode.isHidden = false } private func updateIsInProgress(_ value: Bool) { if value { if self.statusController == nil { let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) self.statusController = statusController self.controller?.present(statusController, in: .window(.root)) } } else if let statusController = self.statusController { self.statusController = nil statusController.dismiss() } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets let bottomPanelHorizontalInset: CGFloat = 16.0 let bottomPanelVerticalInset: CGFloat = 16.0 let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight))) transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition) updatedInsets.bottom = bottomPanelHeight super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets) transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame) } @objc func actionButtonPressed() { self.pay() } private func pay(savedCredentialsToken: TemporaryTwoStepPasswordToken? = nil, liabilityNoticeAccepted: Bool = false, receivedCredentials: BotPaymentCredentials? = nil) { guard let paymentForm = self.paymentFormValue else { return } if !paymentForm.invoice.requestedFields.isEmpty { guard let validatedFormInfo = self.currentValidatedFormInfo else { if paymentForm.invoice.requestedFields.contains(.shippingAddress) { self.arguments?.openInfo(.address(.street1)) } else if paymentForm.invoice.requestedFields.contains(.name) { self.arguments?.openInfo(.name) } else if paymentForm.invoice.requestedFields.contains(.email) { self.arguments?.openInfo(.email) } else if paymentForm.invoice.requestedFields.contains(.phone) { self.arguments?.openInfo(.phone) } return } if let _ = validatedFormInfo.shippingOptions { if self.currentShippingOptionId == nil { self.arguments?.openShippingMethod() return } } } guard let paymentMethod = self.currentPaymentMethod else { self.arguments?.openPaymentMethod() return } let credentials: BotPaymentCredentials if let receivedCredentials = receivedCredentials { credentials = receivedCredentials } else { switch paymentMethod { case let .savedCredentials(savedCredentials): switch savedCredentials { case let .card(id, title): if let savedCredentialsToken = savedCredentialsToken { credentials = .saved(id: id, tempPassword: savedCredentialsToken.token) } else { let _ = (cachedTwoStepPasswordToken(postbox: self.context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self { let timestamp = strongSelf.context.account.network.getApproximateRemoteTimestamp() if let token = token, token.validUntilDate > timestamp - 1 * 60 { if token.requiresBiometrics { let reasonText: String if let biometricAuthentication = LocalAuth.biometricAuthentication, case .faceId = biometricAuthentication { reasonText = strongSelf.presentationData.strings.Checkout_PayWithFaceId } else { reasonText = strongSelf.presentationData.strings.Checkout_PayWithTouchId } let _ = (LocalAuth.auth(reason: reasonText) |> deliverOnMainQueue).start(next: { value, _ in if let strongSelf = self { if value { strongSelf.pay(savedCredentialsToken: token) } else { strongSelf.requestPassword(cardTitle: title) } } }) } else { strongSelf.pay(savedCredentialsToken: token) } } else { strongSelf.requestPassword(cardTitle: title) } } }) return } } case let .webToken(token): credentials = .generic(data: token.data, saveOnServer: token.saveOnServer) case .applePay: guard let paymentForm = self.paymentFormValue, let nativeProvider = paymentForm.nativeProvider else { return } guard let nativeParamsData = nativeProvider.params.data(using: .utf8) else { return } guard let nativeParams = (try? JSONSerialization.jsonObject(with: nativeParamsData, options: [])) as? [String: Any] else { return } let merchantId: String if nativeProvider.name == "stripe" { merchantId = "merchant.ph.telegra.Telegraph" } else if let paramsId = nativeParams["apple_pay_merchant_id"] as? String { merchantId = paramsId } else { return } let botPeerId = self.messageId.peerId let _ = (self.context.account.postbox.transaction({ transaction -> Peer? in return transaction.getPeer(botPeerId) }) |> deliverOnMainQueue).start(next: { [weak self] botPeer in if let strongSelf = self, let botPeer = botPeer { let request = PKPaymentRequest() request.merchantIdentifier = merchantId request.supportedNetworks = [.visa, .amex, .masterCard] request.merchantCapabilities = [.capability3DS] request.countryCode = "US" request.currencyCode = paymentForm.invoice.currency.uppercased() var items: [PKPaymentSummaryItem] = [] var totalAmount: Int64 = 0 for price in paymentForm.invoice.prices { totalAmount += price.amount if let fractional = currencyToFractionalAmount(value: price.amount, currency: paymentForm.invoice.currency) { let amount = NSDecimalNumber(value: fractional) items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) } } if let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, let shippingOptionId = strongSelf.currentShippingOptionId { if let shippingOptionIndex = shippingOptions.firstIndex(where: { $0.id == shippingOptionId }) { for price in shippingOptions[shippingOptionIndex].prices { totalAmount += price.amount let amount = NSDecimalNumber(value: Double(price.amount) * 0.01) items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) } } } if let tipAmount = strongSelf.currentTipAmount { totalAmount += tipAmount if let fractional = currencyToFractionalAmount(value: tipAmount, currency: paymentForm.invoice.currency) { let amount = NSDecimalNumber(value: fractional) items.append(PKPaymentSummaryItem(label: strongSelf.presentationData.strings.Checkout_TipItem, amount: amount)) } } if let fractionalTotal = currencyToFractionalAmount(value: totalAmount, currency: paymentForm.invoice.currency) { let amount = NSDecimalNumber(value: fractionalTotal) items.append(PKPaymentSummaryItem(label: botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), amount: amount)) } request.paymentSummaryItems = items if let controller = PKPaymentAuthorizationViewController(paymentRequest: request) { controller.delegate = strongSelf if let window = strongSelf.view.window { strongSelf.applePayController = controller controller.popoverPresentationController?.sourceView = window controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) window.rootViewController?.present(controller, animated: true) } } } }) return } } if !liabilityNoticeAccepted { let botPeer: Signal = self.context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(paymentForm.paymentBotId) } let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(accountManager: self.context.sharedContext.accountManager, peerId: paymentForm.paymentBotId), botPeer, self.context.account.postbox.loadedPeerWithId(paymentForm.providerId)) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in if let strongSelf = self, let botPeer = botPeer { if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } else { let paymentText = strongSelf.presentationData.strings.Checkout_PaymentLiabilityAlert .replacingOccurrences(of: "{target}", with: botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)) .replacingOccurrences(of: "{payment_system}", with: providerPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)) strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: paymentText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotPaymentLiability(accountManager: strongSelf.context.sharedContext.accountManager, peerId: strongSelf.messageId.peerId).start() strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } })]), nil) } } }) } else { self.inProgressDimNode.isUserInteractionEnabled = true self.inProgressDimNode.alpha = 1.0 self.actionButton.isEnabled = false self.updateActionButton() self.updateIsInProgress(true) var tipAmount = self.currentTipAmount if tipAmount == nil, let _ = paymentForm.invoice.tip { tipAmount = 0 } let totalAmount = currentTotalPrice(paymentForm: paymentForm, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) let currencyValue = formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency) self.payDisposable.set((self.context.engine.payments.sendBotPaymentForm(messageId: self.messageId, formId: paymentForm.id, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, tipAmount: tipAmount, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.actionButton.isEnabled = true strongSelf.updateIsInProgress(false) if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { strongSelf.applePayAuthrorizationCompletion = nil applePayAuthrorizationCompletion(.success) } if let applePayController = strongSelf.applePayController { strongSelf.applePayController = nil applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } let proceedWithCompletion: (Bool, MessageId?) -> Void = { success, receiptMessageId in guard let strongSelf = self else { return } if success { strongSelf.dismissAnimated() strongSelf.completed(currencyValue, receiptMessageId) } else { strongSelf.dismissAnimated() } } switch result { case let .done(receiptMessageId): proceedWithCompletion(true, receiptMessageId) case let .externalVerificationRequired(url): strongSelf.updateActionButton() var dismissImpl: ((Bool) -> Void)? let controller = BotCheckoutWebInteractionController(context: strongSelf.context, url: url, intent: .externalVerification({ success in dismissImpl?(success) })) dismissImpl = { [weak controller] success in controller?.dismiss() proceedWithCompletion(success, nil) } strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } }, error: { [weak self] error in if let strongSelf = self { strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.actionButton.isEnabled = true strongSelf.updateActionButton() strongSelf.updateIsInProgress(false) if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { strongSelf.applePayAuthrorizationCompletion = nil applePayAuthrorizationCompletion(.failure) } if let applePayController = strongSelf.applePayController { strongSelf.applePayController = nil applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } let text: String switch error { case .precheckoutFailed: text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed case .paymentFailed: text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed case .alreadyPaid: text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid case .generic: text = strongSelf.presentationData.strings.Checkout_ErrorGeneric } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } })) } } private func requestPassword(cardTitle: String) { let period: Int32 let requiresBiometrics: Bool if LocalAuth.biometricAuthentication != nil { period = 5 * 60 * 60 requiresBiometrics = true } else { period = 1 * 60 * 60 requiresBiometrics = false } self.present(botCheckoutPasswordEntryController(context: self.context, strings: self.presentationData.strings, passwordTip: self.passwordTip, cartTitle: cardTitle, period: period, requiresBiometrics: requiresBiometrics, completion: { [weak self] token in if let strongSelf = self { let durationString = timeIntervalString(strings: strongSelf.presentationData.strings, value: period) let alertText: String if requiresBiometrics { if let biometricAuthentication = LocalAuth.biometricAuthentication, case .faceId = biometricAuthentication { alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeoutAndFaceId(durationString).0 } else { alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeoutAndTouchId(durationString).0 } } else { alertText = strongSelf.presentationData.strings.Checkout_SavePasswordTimeout(durationString).0 } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: alertText, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_No, action: { if let strongSelf = self { strongSelf.pay(savedCredentialsToken: token) } }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { if let strongSelf = self { let _ = cacheTwoStepPasswordToken(postbox: strongSelf.context.account.postbox, token: token).start() strongSelf.pay(savedCredentialsToken: token) } }) ]), nil) } }), nil) } func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) { guard let paymentForm = self.paymentFormValue else { completion(.failure) return } if !formSupportApplePay(paymentForm) { completion(.failure) return } guard let nativeProvider = paymentForm.nativeProvider else { completion(.failure) return } guard let paramsData = nativeProvider.params.data(using: .utf8) else { return } guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { return } if nativeProvider.name == "stripe" { guard let publishableKey = nativeParams["publishable_key"] as? String else { return } let signal: Signal = Signal { subscriber in let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration configuration.smsAutofillDisabled = true configuration.publishableKey = publishableKey configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" let apiClient = STPAPIClient(configuration: configuration) apiClient.createToken(with: payment, completion: { token, error in if let token = token { subscriber.putNext(token) subscriber.putCompletion() } else if let error = error { subscriber.putError(error) } }) return ActionDisposable { } } self.paymentAuthDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self { strongSelf.applePayAuthrorizationCompletion = completion strongSelf.pay(liabilityNoticeAccepted: true, receivedCredentials: .generic(data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: false)) } else { completion(.failure) } }, error: { _ in completion(.failure) })) } else { self.applePayAuthrorizationCompletion = completion guard let paymentString = String(data: payment.token.paymentData, encoding: .utf8) else { return } self.pay(liabilityNoticeAccepted: true, receivedCredentials: .applePay(data: paymentString)) } } func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) { controller.presentingViewController?.dismiss(animated: true, completion: nil) self.paymentAuthDisposable.set(nil) } }