diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 67ec612242..769e802cf8 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -561,7 +561,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing - let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, additionalFields: additionalFields, publishableKey: publishableKey, completion: { method in + let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .stripe(additionalFields: additionalFields, publishableKey: publishableKey), completion: { method in guard let strongSelf = self else { return } @@ -616,6 +616,74 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz 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 diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index c0449e73e2..7ddb525ce3 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -30,13 +30,17 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet { } final class BotCheckoutNativeCardEntryController: ViewController { + enum Provider { + case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String) + case smartglobal(isTesting: Bool, publicToken: String) + } + private var controllerNode: BotCheckoutNativeCardEntryControllerNode { return super.displayNode as! BotCheckoutNativeCardEntryControllerNode } private let context: AccountContext - private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields - private let publishableKey: String + private let provider: Provider private let completion: (BotCheckoutPaymentMethod) -> Void private var presentationData: PresentationData @@ -46,10 +50,9 @@ final class BotCheckoutNativeCardEntryController: ViewController { private var doneItem: UIBarButtonItem? private var activityItem: UIBarButtonItem? - public init(context: AccountContext, additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + public init(context: AccountContext, provider: Provider, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { self.context = context - self.additionalFields = additionalFields - self.publishableKey = publishableKey + self.provider = provider self.completion = completion self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -71,7 +74,7 @@ final class BotCheckoutNativeCardEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = BotCheckoutNativeCardEntryControllerNode(additionalFields: self.additionalFields, publishableKey: self.publishableKey, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in + self.displayNode = BotCheckoutNativeCardEntryControllerNode(provider: self.provider, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismiss: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index d0b2b3e9d5..bbf511a6e5 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -42,7 +42,7 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode { } final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { - private let publishableKey: String + private let provider: BotCheckoutNativeCardEntryController.Provider private let present: (ViewController, Any?) -> Void private let dismiss: () -> Void @@ -70,9 +70,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, private var currentCardData: BotPaymentCardInputData? private var currentCountryIso2: String? + + private var dataTask: URLSessionDataTask? - init(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { - self.publishableKey = publishableKey + init(provider: BotCheckoutNativeCardEntryController.Provider, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + self.provider = provider self.present = present self.dismiss = dismiss @@ -95,46 +97,53 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, cardUpdatedImpl?(data) } itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem]) - - if additionalFields.contains(.cardholderName) { - var sectionItems: [BotPaymentItemNode] = [] - - sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) - - let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) - self.cardholderItem = cardholderItem - sectionItems.append(cardholderItem) - - itemNodes.append(sectionItems) - } else { - self.cardholderItem = nil - } - - if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { - var sectionItems: [BotPaymentItemNode] = [] - - sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) - - if additionalFields.contains(.country) { - let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") - countryItem.action = { - openCountrySelectionImpl?() + + switch provider { + case let .stripe(additionalFields, _): + if additionalFields.contains(.cardholderName) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) + + let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) + self.cardholderItem = cardholderItem + sectionItems.append(cardholderItem) + + itemNodes.append(sectionItems) + } else { + self.cardholderItem = nil + } + + if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) + + if additionalFields.contains(.country) { + let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") + countryItem.action = { + openCountrySelectionImpl?() + } + self.countryItem = countryItem + sectionItems.append(countryItem) + } else { + self.countryItem = nil } - self.countryItem = countryItem - sectionItems.append(countryItem) + if additionalFields.contains(.zipCode) { + let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address) + self.zipCodeItem = zipCodeItem + sectionItems.append(zipCodeItem) + } else { + self.zipCodeItem = nil + } + + itemNodes.append(sectionItems) } else { self.countryItem = nil - } - if additionalFields.contains(.zipCode) { - let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address) - self.zipCodeItem = zipCodeItem - sectionItems.append(zipCodeItem) - } else { self.zipCodeItem = nil } - - itemNodes.append(sectionItems) - } else { + case .smartglobal: + self.cardholderItem = nil self.countryItem = nil self.zipCodeItem = nil } @@ -214,6 +223,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, deinit { self.verifyDisposable.dispose() + self.dataTask?.cancel() } func updateCountry(_ iso2: String) { @@ -232,53 +242,149 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, guard let cardData = self.currentCardData else { return } - - let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration - configuration.smsAutofillDisabled = true - configuration.publishableKey = self.publishableKey - configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" - - let apiClient = STPAPIClient(configuration: configuration) - - let card = STPCardParams() - card.number = cardData.number - card.cvc = cardData.code - card.expYear = cardData.year - card.expMonth = cardData.month - card.name = self.cardholderItem?.text - card.addressCountry = self.currentCountryIso2 - card.addressZip = self.zipCodeItem?.text - - let createToken: Signal = Signal { subscriber in - apiClient.createToken(withCard: card, completion: { token, error in - if let error = error { - subscriber.putError(error) - } else if let token = token { - subscriber.putNext(token) - subscriber.putCompletion() + + switch self.provider { + case let .stripe(_, publishableKey): + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + + let apiClient = STPAPIClient(configuration: configuration) + + let card = STPCardParams() + card.number = cardData.number + card.cvc = cardData.code + card.expYear = cardData.year + card.expMonth = cardData.month + card.name = self.cardholderItem?.text + card.addressCountry = self.currentCountryIso2 + card.addressZip = self.zipCodeItem?.text + + let createToken: Signal = Signal { subscriber in + apiClient.createToken(withCard: card, completion: { token, error in + if let error = error { + subscriber.putError(error) + } else if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } + }) + + return ActionDisposable { + let _ = apiClient.publishableKey + } + } + + self.isVerifying = true + self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self, let card = token.card { + let last4 = card.last4() + let brand = STPAPIClient.string(with: card.brand) + strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn))) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.isVerifying = false + strongSelf.updateDone() + } + })) + + self.updateDone() + case let .smartglobal(isTesting, publicToken): + let url: String + if isTesting { + url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card" + } else { + url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card" + } + + let jsonPayload: [String: Any] = [ + "card": [ + "number": cardData.number, + "expiration_month": "\(cardData.month)", + "expiration_year": "\(cardData.year)", + "security_code": "\(cardData.code)" + ] as [String: Any] + ] + + guard let parsedUrl = URL(string: url) else { + return + } + + var request = URLRequest(url: parsedUrl) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(publicToken, forHTTPHeaderField: "X-PUBLIC-TOKEN") + guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonPayload, options: []) else { + return + } + request.httpBody = requestBody + + let session = URLSession.shared + let dataTask = session.dataTask(with: request, completionHandler: { [weak self] data, response, error in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + enum ReponseError: Error { + case generic + } + + do { + guard let data = data else { + throw ReponseError.generic + } + + let jsonRaw = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonRaw as? [String: Any] else { + throw ReponseError.generic + } + guard let resultData = json["data"] as? [String: Any] else { + throw ReponseError.generic + } + guard let resultInfo = resultData["info"] as? [String: Any] else { + throw ReponseError.generic + } + guard let token = resultData["token"] as? String else { + throw ReponseError.generic + } + guard let maskedCardNumber = resultInfo["masked_card_number"] as? String else { + throw ReponseError.generic + } + + let responseJson: [String: Any] = [ + "type": "card", + "id": "\(token)" + ] + + let serializedResponseJson = try JSONSerialization.data(withJSONObject: responseJson, options: []) + + guard let serializedResponseString = String(data: serializedResponseJson, encoding: .utf8) else { + throw ReponseError.generic + } + + strongSelf.completion(.webToken(BotCheckoutPaymentWebToken( + title: maskedCardNumber, + data: serializedResponseString, + saveOnServer: strongSelf.saveInfoItem.isOn + ))) + } catch { + strongSelf.isVerifying = false + strongSelf.updateDone() + } } }) - - return ActionDisposable { - let _ = apiClient.publishableKey - } + self.dataTask = dataTask + + self.isVerifying = true + self.updateDone() + + dataTask.resume() + + break } - - self.isVerifying = true - self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in - if let strongSelf = self, let card = token.card { - let last4 = card.last4() - let brand = STPAPIClient.string(with: card.brand) - strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn))) - } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.isVerifying = false - strongSelf.updateDone() - } - })) - - self.updateDone() } private func updateDone() {