yoomoney_flutter/ios/Classes/SwiftYookassaPaymentsFlutterPlugin.swift
2024-10-17 14:30:32 +03:00

487 lines
18 KiB
Swift

import Flutter
import UIKit
import YooKassaPayments
import Foundation
var flutterResult: FlutterResult?
var tokenizationModuleInput: TokenizationModuleInput?
var flutterController: FlutterViewController?
var yoomoneyController: UIViewController?
public class SwiftYookassaPaymentsFlutterPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "ru.yoomoney.yookassa_payments_flutter/yoomoney", binaryMessenger: registrar.messenger())
let instance = SwiftYookassaPaymentsFlutterPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
flutterResult = result
// Tokenezation Flow
if (call.method == YooMoneyService.tokenization.rawValue) {
guard let data = call.arguments as? [String:AnyObject],
let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
let tokenizationModuleInputData = try? JSONDecoder().decode(TokenizationModuleInputData.self, from: jsonData)
else {
result(YooMoneyErrors.tokenizationData.rawValue)
return
}
let controller = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController
let inputData: TokenizationFlow = .tokenization(tokenizationModuleInputData)
if let flutterVC = controller {
let tokenezationViewController = TokenizationAssembly.makeModule(inputData: inputData, moduleOutput: flutterVC)
yoomoneyController = tokenezationViewController;
flutterController = flutterVC;
flutterVC.present(tokenezationViewController, animated: true, completion: nil)
}
}
// Confirmation Flow
if (call.method == YooMoneyService.confirmation.rawValue) {
guard let data = call.arguments as? [String:AnyObject],
let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
let conformationData = try? JSONDecoder().decode(ConformationData.self, from: jsonData)
else {
result(YooMoneyErrors.conformationData.rawValue)
return
}
var paymentMethod: PaymentMethodType?
switch conformationData.paymentMethod {
case "bankCard":
paymentMethod = .bankCard
case "yooMoney":
paymentMethod = .yooMoney
case "sberbank":
paymentMethod = .sberbank
case "sbp":
paymentMethod = .sbp
default: break
}
guard
let module = tokenizationModuleInput,
let method = paymentMethod,
let url = conformationData.url,
let sheetController = yoomoneyController
else {
result(YooMoneyErrors.navigation.rawValue)
return
}
let controller = UIApplication.shared.delegate?.window??.rootViewController as! FlutterViewController
controller.present(sheetController, animated: true, completion: nil)
module.startConfirmationProcess(
confirmationUrl: url,
paymentMethodType: method
)
}
// BankCardRepeat Flow
if (call.method == YooMoneyService.repeatPayment.rawValue) {
guard let data = call.arguments as? [String:AnyObject],
let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
let bankCardRepeatModuleInputData = try? JSONDecoder().decode(BankCardRepeatModuleInputData.self, from: jsonData)
else {
result(YooMoneyErrors.repeatPaymentData.rawValue)
return
}
let inputData: TokenizationFlow = .bankCardRepeat(bankCardRepeatModuleInputData)
flutterController = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController
if let controller = flutterController {
let vc = TokenizationAssembly.makeModule(inputData: inputData, moduleOutput: controller)
yoomoneyController = vc
tokenizationModuleInput = vc
controller.present(vc, animated: true, completion: nil)
}
}
}
}
extension FlutterViewController: TokenizationModuleOutput {
public func didFailConfirmation(error: YooKassaPayments.YooKassaPaymentsError?) {
DispatchQueue.main.async {
if let controller = yoomoneyController {
controller.dismiss(animated: true)
}
}
guard let result = flutterResult else { return }
if let error = error {
result("{\"status\":\"error\", \"error\": \"\(error.localizedDescription)\"}")
}
}
public func tokenizationModule(
_ module: TokenizationModuleInput,
didTokenize token: Tokens,
paymentMethodType: PaymentMethodType
) {
tokenizationModuleInput = module
if let result = flutterResult {
result("{\"status\":\"success\", \"paymentToken\": \"\(token.paymentToken)\", \"paymentMethodType\": \"\(paymentMethodType.rawValue)\"}")
DispatchQueue.main.async {
if let controller = yoomoneyController {
controller.dismiss(animated: true)
}
}
}
}
public func didFinish(
on module: TokenizationModuleInput,
with error: YooKassaPaymentsError?
) {
DispatchQueue.main.async {
if let controller = yoomoneyController {
controller.dismiss(animated: true)
}
}
guard let result = flutterResult else { return }
if let error = error {
result("{\"status\":\"error\", \"error\": \"\(error.localizedDescription)\"}")
} else {
result("{\"status\":\"canceled\"}")
}
}
public func didFinishConfirmation(paymentMethodType: PaymentMethodType) {
guard let result = flutterResult else { return }
DispatchQueue.main.async {
if let controller = yoomoneyController {
controller.dismiss(animated: true)
}
}
result("{\"paymentMethodType\": \"\(paymentMethodType.rawValue)\"}")
}
}
struct HostParameters: Codable, Equatable {
let host: String?
let paymentAuthorizationHost: String?
let authHost: String?
let configHost: String?
enum CodingKeys: String, CodingKey {
case host = "host"
case paymentAuthorizationHost = "paymentAuthorizationHost"
case authHost = "authHost"
case configHost = "configHost"
}
}
struct ConformationData: Codable, Equatable {
let url: String?
let paymentMethod: String?
enum CodingKeys: String, CodingKey {
case url = "url"
case paymentMethod = "paymentMethod"
}
}
enum YooMoneyService: String {
case tokenization = "tokenization"
case confirmation = "confirmation"
case repeatPayment = "repeat"
}
enum YooMoneyErrors: String {
case navigation = "ErrorNavigation"
case tokenizationData = "ErrorTokenizationData"
case conformationData = "ErrorConfirmationData"
case repeatPaymentData = "ErrorRepeatPaymentData"
case tokenizationResult = "ErrorTokenizationResult"
}
extension TokenizationModuleInputData: Decodable {
enum CodingKeys: String, CodingKey {
case clientApplicationKey = "clientApplicationKey"
case shopName = "title"
case shopId = "shopId"
case purchaseDescription = "subtitle"
case amount = "amount"
case savePaymentMethod = "savePaymentMethod"
case gatewayId = "gatewayId"
case tokenizationSettings = "tokenizationSettings"
case testModeSettings = "testModeSettings"
case applePayMerchantIdentifier = "applePayMerchantIdentifier"
case returnUrl = "returnUrl"
case isLoggingEnabled = "isLoggingEnabled"
case userPhoneNumber = "userPhoneNumber"
case customizationSettings = "customizationSettings"
case moneyAuthClientId = "moneyAuthClientId"
case applicationScheme = "applicationScheme"
case customerId = "customerId"
case hostParameters = "hostParameters"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let clientApplicationKey = try values.decode(String.self, forKey: .clientApplicationKey)
let shopName = try values.decode(String.self, forKey: .shopName)
let shopId = try values.decode(String.self, forKey: .shopId)
let purchaseDescription = try values.decode(String.self, forKey: .purchaseDescription)
let amount = try values.decode(Amount.self, forKey: .amount)
let gatewayId = try? values.decode(String.self, forKey: .gatewayId)
let settings = try values.decode(TokenizationSettings.self, forKey: .tokenizationSettings)
let tokenizationSettings = TokenizationSettings(paymentMethodTypes: settings.paymentMethodTypes)
let testModeSettings = try? values.decode(TestModeSettings.self, forKey: .testModeSettings)
let applePayMerchantIdentifier = try? values.decode(String.self, forKey: .applePayMerchantIdentifier)
let returnUrl = try? values.decode(String.self, forKey: .returnUrl)
let isLoggingEnabled = try values.decode(Bool.self, forKey: .isLoggingEnabled)
let userPhoneNumber = try? values.decode(String.self, forKey: .userPhoneNumber)
let customizationSettings = try values.decode(CustomizationSettings.self, forKey: .customizationSettings)
let moneyAuthClientId = try? values.decode(String.self, forKey: .moneyAuthClientId)
let applicationScheme = try? values.decode(String.self, forKey: .applicationScheme)
let customerId = try? values.decode(String.self, forKey: .customerId)
let hostParameters = try? values.decode(HostParameters.self, forKey: .hostParameters)
var savePaymentMethod: SavePaymentMethod
switch try values.decode(String.self, forKey: .savePaymentMethod) {
case "SavePaymentMethod.on":
savePaymentMethod = .on
case "SavePaymentMethod.off":
savePaymentMethod = .off
default:
savePaymentMethod = .userSelects
}
let userDefaults = UserDefaults.standard
userDefaults.set(hostParameters != nil, forKey: "dev_host_preference")
userDefaults.synchronize()
self.init(
clientApplicationKey: clientApplicationKey,
shopName: shopName,
shopId: shopId,
purchaseDescription: purchaseDescription,
amount: amount,
gatewayId: gatewayId,
tokenizationSettings: tokenizationSettings,
testModeSettings: testModeSettings,
returnUrl: returnUrl,
isLoggingEnabled: isLoggingEnabled,
userPhoneNumber: userPhoneNumber,
customizationSettings: customizationSettings,
savePaymentMethod: savePaymentMethod,
moneyAuthClientId: moneyAuthClientId,
applicationScheme: applicationScheme,
customerId: customerId
)
}
}
extension BankCardRepeatModuleInputData: Decodable {
enum CodingKeys: String, CodingKey {
case clientApplicationKey = "clientApplicationKey"
case shopName = "title"
case purchaseDescription = "subtitle"
case amount = "amount"
case savePaymentMethod = "savePaymentMethod"
case paymentMethodId = "paymentMethodId"
case gatewayId = "gatewayId"
case testModeSettings = "testModeSettings"
case returnUrl = "returnUrl"
case isLoggingEnabled = "isLoggingEnabled"
case customizationSettings = "customizationSettings"
case cardScanning = "cardScanning"
case isSafeDeal = "isSafeDeal"
case customerId = "customerId"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let clientApplicationKey = try values.decode(String.self, forKey: .clientApplicationKey)
let shopName = try values.decode(String.self, forKey: .shopName)
let purchaseDescription = try values.decode(String.self, forKey: .purchaseDescription)
let amount = try values.decode(Amount.self, forKey: .amount)
let paymentMethodId = try values.decode(String.self, forKey: .paymentMethodId)
let gatewayId = try? values.decode(String.self, forKey: .gatewayId)
let testModeSettings = try? values.decode(TestModeSettings.self, forKey: .testModeSettings)
let returnUrl = try? values.decode(String.self, forKey: .returnUrl)
let isLoggingEnabled = try values.decode(Bool.self, forKey: .isLoggingEnabled)
let customizationSettings = try values.decode(CustomizationSettings.self, forKey: .customizationSettings)
var savePaymentMethod: SavePaymentMethod
switch try values.decode(String.self, forKey: .savePaymentMethod) {
case "SavePaymentMethod.on":
savePaymentMethod = .on
case "SavePaymentMethod.off":
savePaymentMethod = .off
default:
savePaymentMethod = .userSelects
}
self.init(
clientApplicationKey: clientApplicationKey,
shopName: shopName,
purchaseDescription: purchaseDescription,
paymentMethodId: paymentMethodId,
amount: amount,
testModeSettings: testModeSettings,
returnUrl: returnUrl,
isLoggingEnabled: isLoggingEnabled,
customizationSettings: customizationSettings,
savePaymentMethod: savePaymentMethod,
gatewayId: gatewayId
)
}
}
extension Amount: Decodable {
enum CodingKeys: String, CodingKey {
case value = "value"
case currency = "currency"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let stringValue = try values.decode(String.self, forKey: .value)
guard let decimalValue = Decimal(string: stringValue) else { throw CommonError.decodingError }
let currency = try values.decode(String.self, forKey: .currency)
self.init(value: decimalValue, currency: .custom(currency))
}
}
extension TokenizationSettings: Decodable {
enum CodingKeys: String, CodingKey {
case paymentMethodTypes = "paymentMethodTypes"
case showYooKassaLogo = "showYooKassaLogo"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let paymentFlutterMethodTypes = try values.decode([String].self, forKey: .paymentMethodTypes)
var paymentTypes: PaymentMethodTypes = []
for type in paymentFlutterMethodTypes {
switch type {
case "PaymentMethod.bankCard":
paymentTypes.insert(.bankCard)
case "PaymentMethod.yooMoney":
paymentTypes.insert(.yooMoney)
case "PaymentMethod.sberbank":
paymentTypes.insert(.sberbank)
case "PaymentMethod.sbp":
paymentTypes.insert(.sbp)
default: break
}
}
self.init(paymentMethodTypes: paymentTypes)
}
}
extension TestModeSettings: Decodable {
enum CodingKeys: String, CodingKey {
case paymentAuthorizationPassed = "paymentAuthorizationPassed"
case cardsCount = "cardsCount"
case charge = "charge"
case enablePaymentError = "enablePaymentError"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let paymentAuthorizationPassed = try values.decode(Bool.self, forKey: .paymentAuthorizationPassed)
let cardsCount = try values.decode(Int.self, forKey: .cardsCount)
let charge = try values.decode(Amount.self, forKey: .charge)
let enablePaymentError = try values.decode(Bool.self, forKey: .enablePaymentError)
self.init(
paymentAuthorizationPassed: paymentAuthorizationPassed,
cardsCount: cardsCount,
charge: charge,
enablePaymentError: enablePaymentError
)
}
}
extension CustomizationSettings: Decodable {
enum CodingKeys: String, CodingKey {
case mainScheme = "mainScheme"
case showYooKassaLogo = "showYooKassaLogo"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let schemeColor = try values.decode(Color.self, forKey: .mainScheme)
let showYooKassaLogo = try values.decode(Bool.self, forKey: .showYooKassaLogo)
self.init(mainScheme:
UIColor(
red: schemeColor.red,
green: schemeColor.green,
blue: schemeColor.blue,
alpha: schemeColor.alpha
),
showYooKassaLogo: showYooKassaLogo
)
}
}
public enum CommonError: Error {
case decodingError
}
struct Color: Decodable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat
init(
red: CGFloat,
green: CGFloat,
blue: CGFloat,
alpha: CGFloat
) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
enum CodingKeys: String, CodingKey {
case red = "red"
case blue = "blue"
case green = "green"
case alpha = "alpha"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let red = try values.decode(CGFloat.self, forKey: .red)
let blue = try values.decode(CGFloat.self, forKey: .blue)
let green = try values.decode(CGFloat.self, forKey: .green)
let alpha = try values.decode(CGFloat.self, forKey: .alpha)
self.init(red: red / 255,
green: green / 255,
blue: blue / 255,
alpha: alpha
)
}
}