SwiftYookassaPaymentsFlutterPlugin.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import Flutter
  2. import UIKit
  3. import YooKassaPayments
  4. import Foundation
  5. var flutterResult: FlutterResult?
  6. var tokenizationModuleInput: TokenizationModuleInput?
  7. var flutterController: FlutterViewController?
  8. var yoomoneyController: UIViewController?
  9. public class SwiftYookassaPaymentsFlutterPlugin: NSObject, FlutterPlugin {
  10. public static func register(with registrar: FlutterPluginRegistrar) {
  11. let channel = FlutterMethodChannel(name: "ru.yoomoney.yookassa_payments_flutter/yoomoney", binaryMessenger: registrar.messenger())
  12. let instance = SwiftYookassaPaymentsFlutterPlugin()
  13. registrar.addMethodCallDelegate(instance, channel: channel)
  14. }
  15. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
  16. flutterResult = result
  17. // Tokenezation Flow
  18. if (call.method == YooMoneyService.tokenization.rawValue) {
  19. guard let data = call.arguments as? [String:AnyObject],
  20. let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
  21. let tokenizationModuleInputData = try? JSONDecoder().decode(TokenizationModuleInputData.self, from: jsonData)
  22. else {
  23. result(YooMoneyErrors.tokenizationData.rawValue)
  24. return
  25. }
  26. let controller = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController
  27. let inputData: TokenizationFlow = .tokenization(tokenizationModuleInputData)
  28. if let flutterVC = controller {
  29. let tokenezationViewController = TokenizationAssembly.makeModule(inputData: inputData, moduleOutput: flutterVC)
  30. yoomoneyController = tokenezationViewController;
  31. flutterController = flutterVC;
  32. flutterVC.present(tokenezationViewController, animated: true, completion: nil)
  33. }
  34. }
  35. // Confirmation Flow
  36. if (call.method == YooMoneyService.confirmation.rawValue) {
  37. guard let data = call.arguments as? [String:AnyObject],
  38. let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
  39. let conformationData = try? JSONDecoder().decode(ConformationData.self, from: jsonData)
  40. else {
  41. result(YooMoneyErrors.conformationData.rawValue)
  42. return
  43. }
  44. var paymentMethod: PaymentMethodType?
  45. switch conformationData.paymentMethod {
  46. case "bankCard":
  47. paymentMethod = .bankCard
  48. case "yooMoney":
  49. paymentMethod = .yooMoney
  50. case "sberbank":
  51. paymentMethod = .sberbank
  52. case "sbp":
  53. paymentMethod = .sbp
  54. default: break
  55. }
  56. guard
  57. let module = tokenizationModuleInput,
  58. let method = paymentMethod,
  59. let url = conformationData.url,
  60. let sheetController = yoomoneyController
  61. else {
  62. result(YooMoneyErrors.navigation.rawValue)
  63. return
  64. }
  65. let controller = UIApplication.shared.delegate?.window??.rootViewController as! FlutterViewController
  66. controller.present(sheetController, animated: true, completion: nil)
  67. module.startConfirmationProcess(
  68. confirmationUrl: url,
  69. paymentMethodType: method
  70. )
  71. }
  72. // BankCardRepeat Flow
  73. if (call.method == YooMoneyService.repeatPayment.rawValue) {
  74. guard let data = call.arguments as? [String:AnyObject],
  75. let jsonData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted),
  76. let bankCardRepeatModuleInputData = try? JSONDecoder().decode(BankCardRepeatModuleInputData.self, from: jsonData)
  77. else {
  78. result(YooMoneyErrors.repeatPaymentData.rawValue)
  79. return
  80. }
  81. let inputData: TokenizationFlow = .bankCardRepeat(bankCardRepeatModuleInputData)
  82. flutterController = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController
  83. if let controller = flutterController {
  84. let vc = TokenizationAssembly.makeModule(inputData: inputData, moduleOutput: controller)
  85. yoomoneyController = vc
  86. tokenizationModuleInput = vc
  87. controller.present(vc, animated: true, completion: nil)
  88. }
  89. }
  90. }
  91. }
  92. extension FlutterViewController: TokenizationModuleOutput {
  93. public func didFailConfirmation(error: YooKassaPayments.YooKassaPaymentsError?) {
  94. DispatchQueue.main.async {
  95. if let controller = yoomoneyController {
  96. controller.dismiss(animated: true)
  97. }
  98. }
  99. guard let result = flutterResult else { return }
  100. if let error = error {
  101. result("{\"status\":\"error\", \"error\": \"\(error.localizedDescription)\"}")
  102. }
  103. }
  104. public func tokenizationModule(
  105. _ module: TokenizationModuleInput,
  106. didTokenize token: Tokens,
  107. paymentMethodType: PaymentMethodType
  108. ) {
  109. tokenizationModuleInput = module
  110. if let result = flutterResult {
  111. result("{\"status\":\"success\", \"paymentToken\": \"\(token.paymentToken)\", \"paymentMethodType\": \"\(paymentMethodType.rawValue)\"}")
  112. DispatchQueue.main.async {
  113. if let controller = yoomoneyController {
  114. controller.dismiss(animated: true)
  115. }
  116. }
  117. }
  118. }
  119. public func didFinish(
  120. on module: TokenizationModuleInput,
  121. with error: YooKassaPaymentsError?
  122. ) {
  123. DispatchQueue.main.async {
  124. if let controller = yoomoneyController {
  125. controller.dismiss(animated: true)
  126. }
  127. }
  128. guard let result = flutterResult else { return }
  129. if let error = error {
  130. result("{\"status\":\"error\", \"error\": \"\(error.localizedDescription)\"}")
  131. } else {
  132. result("{\"status\":\"canceled\"}")
  133. }
  134. }
  135. public func didFinishConfirmation(paymentMethodType: PaymentMethodType) {
  136. guard let result = flutterResult else { return }
  137. DispatchQueue.main.async {
  138. if let controller = yoomoneyController {
  139. controller.dismiss(animated: true)
  140. }
  141. }
  142. result("{\"paymentMethodType\": \"\(paymentMethodType.rawValue)\"}")
  143. }
  144. }
  145. struct HostParameters: Codable, Equatable {
  146. let host: String?
  147. let paymentAuthorizationHost: String?
  148. let authHost: String?
  149. let configHost: String?
  150. enum CodingKeys: String, CodingKey {
  151. case host = "host"
  152. case paymentAuthorizationHost = "paymentAuthorizationHost"
  153. case authHost = "authHost"
  154. case configHost = "configHost"
  155. }
  156. }
  157. struct ConformationData: Codable, Equatable {
  158. let url: String?
  159. let paymentMethod: String?
  160. enum CodingKeys: String, CodingKey {
  161. case url = "url"
  162. case paymentMethod = "paymentMethod"
  163. }
  164. }
  165. enum YooMoneyService: String {
  166. case tokenization = "tokenization"
  167. case confirmation = "confirmation"
  168. case repeatPayment = "repeat"
  169. }
  170. enum YooMoneyErrors: String {
  171. case navigation = "ErrorNavigation"
  172. case tokenizationData = "ErrorTokenizationData"
  173. case conformationData = "ErrorConfirmationData"
  174. case repeatPaymentData = "ErrorRepeatPaymentData"
  175. case tokenizationResult = "ErrorTokenizationResult"
  176. }
  177. extension TokenizationModuleInputData: Decodable {
  178. enum CodingKeys: String, CodingKey {
  179. case clientApplicationKey = "clientApplicationKey"
  180. case shopName = "title"
  181. case shopId = "shopId"
  182. case purchaseDescription = "subtitle"
  183. case amount = "amount"
  184. case savePaymentMethod = "savePaymentMethod"
  185. case gatewayId = "gatewayId"
  186. case tokenizationSettings = "tokenizationSettings"
  187. case testModeSettings = "testModeSettings"
  188. case applePayMerchantIdentifier = "applePayMerchantIdentifier"
  189. case returnUrl = "returnUrl"
  190. case isLoggingEnabled = "isLoggingEnabled"
  191. case userPhoneNumber = "userPhoneNumber"
  192. case customizationSettings = "customizationSettings"
  193. case moneyAuthClientId = "moneyAuthClientId"
  194. case applicationScheme = "applicationScheme"
  195. case customerId = "customerId"
  196. case hostParameters = "hostParameters"
  197. }
  198. public init(from decoder: Decoder) throws {
  199. let values = try decoder.container(keyedBy: CodingKeys.self)
  200. let clientApplicationKey = try values.decode(String.self, forKey: .clientApplicationKey)
  201. let shopName = try values.decode(String.self, forKey: .shopName)
  202. let shopId = try values.decode(String.self, forKey: .shopId)
  203. let purchaseDescription = try values.decode(String.self, forKey: .purchaseDescription)
  204. let amount = try values.decode(Amount.self, forKey: .amount)
  205. let gatewayId = try? values.decode(String.self, forKey: .gatewayId)
  206. let settings = try values.decode(TokenizationSettings.self, forKey: .tokenizationSettings)
  207. let tokenizationSettings = TokenizationSettings(paymentMethodTypes: settings.paymentMethodTypes)
  208. let testModeSettings = try? values.decode(TestModeSettings.self, forKey: .testModeSettings)
  209. let applePayMerchantIdentifier = try? values.decode(String.self, forKey: .applePayMerchantIdentifier)
  210. let returnUrl = try? values.decode(String.self, forKey: .returnUrl)
  211. let isLoggingEnabled = try values.decode(Bool.self, forKey: .isLoggingEnabled)
  212. let userPhoneNumber = try? values.decode(String.self, forKey: .userPhoneNumber)
  213. let customizationSettings = try values.decode(CustomizationSettings.self, forKey: .customizationSettings)
  214. let moneyAuthClientId = try? values.decode(String.self, forKey: .moneyAuthClientId)
  215. let applicationScheme = try? values.decode(String.self, forKey: .applicationScheme)
  216. let customerId = try? values.decode(String.self, forKey: .customerId)
  217. let hostParameters = try? values.decode(HostParameters.self, forKey: .hostParameters)
  218. var savePaymentMethod: SavePaymentMethod
  219. switch try values.decode(String.self, forKey: .savePaymentMethod) {
  220. case "SavePaymentMethod.on":
  221. savePaymentMethod = .on
  222. case "SavePaymentMethod.off":
  223. savePaymentMethod = .off
  224. default:
  225. savePaymentMethod = .userSelects
  226. }
  227. let userDefaults = UserDefaults.standard
  228. userDefaults.set(hostParameters != nil, forKey: "dev_host_preference")
  229. userDefaults.synchronize()
  230. self.init(
  231. clientApplicationKey: clientApplicationKey,
  232. shopName: shopName,
  233. shopId: shopId,
  234. purchaseDescription: purchaseDescription,
  235. amount: amount,
  236. gatewayId: gatewayId,
  237. tokenizationSettings: tokenizationSettings,
  238. testModeSettings: testModeSettings,
  239. returnUrl: returnUrl,
  240. isLoggingEnabled: isLoggingEnabled,
  241. userPhoneNumber: userPhoneNumber,
  242. customizationSettings: customizationSettings,
  243. savePaymentMethod: savePaymentMethod,
  244. moneyAuthClientId: moneyAuthClientId,
  245. applicationScheme: applicationScheme,
  246. customerId: customerId
  247. )
  248. }
  249. }
  250. extension BankCardRepeatModuleInputData: Decodable {
  251. enum CodingKeys: String, CodingKey {
  252. case clientApplicationKey = "clientApplicationKey"
  253. case shopName = "title"
  254. case purchaseDescription = "subtitle"
  255. case amount = "amount"
  256. case savePaymentMethod = "savePaymentMethod"
  257. case paymentMethodId = "paymentMethodId"
  258. case gatewayId = "gatewayId"
  259. case testModeSettings = "testModeSettings"
  260. case returnUrl = "returnUrl"
  261. case isLoggingEnabled = "isLoggingEnabled"
  262. case customizationSettings = "customizationSettings"
  263. case cardScanning = "cardScanning"
  264. case isSafeDeal = "isSafeDeal"
  265. case customerId = "customerId"
  266. }
  267. public init(from decoder: Decoder) throws {
  268. let values = try decoder.container(keyedBy: CodingKeys.self)
  269. let clientApplicationKey = try values.decode(String.self, forKey: .clientApplicationKey)
  270. let shopName = try values.decode(String.self, forKey: .shopName)
  271. let purchaseDescription = try values.decode(String.self, forKey: .purchaseDescription)
  272. let amount = try values.decode(Amount.self, forKey: .amount)
  273. let paymentMethodId = try values.decode(String.self, forKey: .paymentMethodId)
  274. let gatewayId = try? values.decode(String.self, forKey: .gatewayId)
  275. let testModeSettings = try? values.decode(TestModeSettings.self, forKey: .testModeSettings)
  276. let returnUrl = try? values.decode(String.self, forKey: .returnUrl)
  277. let isLoggingEnabled = try values.decode(Bool.self, forKey: .isLoggingEnabled)
  278. let customizationSettings = try values.decode(CustomizationSettings.self, forKey: .customizationSettings)
  279. var savePaymentMethod: SavePaymentMethod
  280. switch try values.decode(String.self, forKey: .savePaymentMethod) {
  281. case "SavePaymentMethod.on":
  282. savePaymentMethod = .on
  283. case "SavePaymentMethod.off":
  284. savePaymentMethod = .off
  285. default:
  286. savePaymentMethod = .userSelects
  287. }
  288. self.init(
  289. clientApplicationKey: clientApplicationKey,
  290. shopName: shopName,
  291. purchaseDescription: purchaseDescription,
  292. paymentMethodId: paymentMethodId,
  293. amount: amount,
  294. testModeSettings: testModeSettings,
  295. returnUrl: returnUrl,
  296. isLoggingEnabled: isLoggingEnabled,
  297. customizationSettings: customizationSettings,
  298. savePaymentMethod: savePaymentMethod,
  299. gatewayId: gatewayId
  300. )
  301. }
  302. }
  303. extension Amount: Decodable {
  304. enum CodingKeys: String, CodingKey {
  305. case value = "value"
  306. case currency = "currency"
  307. }
  308. public init(from decoder: Decoder) throws {
  309. let values = try decoder.container(keyedBy: CodingKeys.self)
  310. let stringValue = try values.decode(String.self, forKey: .value)
  311. guard let decimalValue = Decimal(string: stringValue) else { throw CommonError.decodingError }
  312. let currency = try values.decode(String.self, forKey: .currency)
  313. self.init(value: decimalValue, currency: .custom(currency))
  314. }
  315. }
  316. extension TokenizationSettings: Decodable {
  317. enum CodingKeys: String, CodingKey {
  318. case paymentMethodTypes = "paymentMethodTypes"
  319. case showYooKassaLogo = "showYooKassaLogo"
  320. }
  321. public init(from decoder: Decoder) throws {
  322. let values = try decoder.container(keyedBy: CodingKeys.self)
  323. let paymentFlutterMethodTypes = try values.decode([String].self, forKey: .paymentMethodTypes)
  324. var paymentTypes: PaymentMethodTypes = []
  325. for type in paymentFlutterMethodTypes {
  326. switch type {
  327. case "PaymentMethod.bankCard":
  328. paymentTypes.insert(.bankCard)
  329. case "PaymentMethod.yooMoney":
  330. paymentTypes.insert(.yooMoney)
  331. case "PaymentMethod.sberbank":
  332. paymentTypes.insert(.sberbank)
  333. case "PaymentMethod.sbp":
  334. paymentTypes.insert(.sbp)
  335. default: break
  336. }
  337. }
  338. self.init(paymentMethodTypes: paymentTypes)
  339. }
  340. }
  341. extension TestModeSettings: Decodable {
  342. enum CodingKeys: String, CodingKey {
  343. case paymentAuthorizationPassed = "paymentAuthorizationPassed"
  344. case cardsCount = "cardsCount"
  345. case charge = "charge"
  346. case enablePaymentError = "enablePaymentError"
  347. }
  348. public init(from decoder: Decoder) throws {
  349. let values = try decoder.container(keyedBy: CodingKeys.self)
  350. let paymentAuthorizationPassed = try values.decode(Bool.self, forKey: .paymentAuthorizationPassed)
  351. let cardsCount = try values.decode(Int.self, forKey: .cardsCount)
  352. let charge = try values.decode(Amount.self, forKey: .charge)
  353. let enablePaymentError = try values.decode(Bool.self, forKey: .enablePaymentError)
  354. self.init(
  355. paymentAuthorizationPassed: paymentAuthorizationPassed,
  356. cardsCount: cardsCount,
  357. charge: charge,
  358. enablePaymentError: enablePaymentError
  359. )
  360. }
  361. }
  362. extension CustomizationSettings: Decodable {
  363. enum CodingKeys: String, CodingKey {
  364. case mainScheme = "mainScheme"
  365. case showYooKassaLogo = "showYooKassaLogo"
  366. }
  367. public init(from decoder: Decoder) throws {
  368. let values = try decoder.container(keyedBy: CodingKeys.self)
  369. let schemeColor = try values.decode(Color.self, forKey: .mainScheme)
  370. let showYooKassaLogo = try values.decode(Bool.self, forKey: .showYooKassaLogo)
  371. self.init(mainScheme:
  372. UIColor(
  373. red: schemeColor.red,
  374. green: schemeColor.green,
  375. blue: schemeColor.blue,
  376. alpha: schemeColor.alpha
  377. ),
  378. showYooKassaLogo: showYooKassaLogo
  379. )
  380. }
  381. }
  382. public enum CommonError: Error {
  383. case decodingError
  384. }
  385. struct Color: Decodable {
  386. let red: CGFloat
  387. let green: CGFloat
  388. let blue: CGFloat
  389. let alpha: CGFloat
  390. init(
  391. red: CGFloat,
  392. green: CGFloat,
  393. blue: CGFloat,
  394. alpha: CGFloat
  395. ) {
  396. self.red = red
  397. self.green = green
  398. self.blue = blue
  399. self.alpha = alpha
  400. }
  401. enum CodingKeys: String, CodingKey {
  402. case red = "red"
  403. case blue = "blue"
  404. case green = "green"
  405. case alpha = "alpha"
  406. }
  407. init(from decoder: Decoder) throws {
  408. let values = try decoder.container(keyedBy: CodingKeys.self)
  409. let red = try values.decode(CGFloat.self, forKey: .red)
  410. let blue = try values.decode(CGFloat.self, forKey: .blue)
  411. let green = try values.decode(CGFloat.self, forKey: .green)
  412. let alpha = try values.decode(CGFloat.self, forKey: .alpha)
  413. self.init(red: red / 255,
  414. green: green / 255,
  415. blue: blue / 255,
  416. alpha: alpha
  417. )
  418. }
  419. }