LNExchangePanel.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. //
  2. // LNExchangePanel.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2026/1/8.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. enum LNExchangeType {
  11. case coinToDiamond
  12. case diamondToCoin
  13. case beanToCoin
  14. case beanToDiamond
  15. var exchangeType: (from: LNCurrencyType, to: LNCurrencyType) {
  16. switch self {
  17. case .coinToDiamond: (.coin, .diamond)
  18. case .diamondToCoin: (.diamond, .coin)
  19. case .beanToCoin: (.bean, .coin)
  20. case .beanToDiamond: (.bean, .diamond)
  21. }
  22. }
  23. static func map(from: LNCurrencyType, to: LNCurrencyType) -> Self? {
  24. if from == .coin, to == .diamond {
  25. .coinToDiamond
  26. } else if from == .diamond, to == .coin {
  27. .diamondToCoin
  28. } else if from == .bean, to == .coin {
  29. .beanToCoin
  30. } else if from == .bean, to == .diamond {
  31. .beanToDiamond
  32. } else {
  33. nil
  34. }
  35. }
  36. var rate: Double? {
  37. let config = LNConfigManager.shared.commonConfig.commonCoinExchangeConsts
  38. switch self {
  39. case .coinToDiamond:
  40. guard let coin = config.first(where: { $0.IDR != nil && $0.goldCoin != nil }),
  41. let diamond = config.first(where: { $0.IDR != nil && $0.diamond != nil })
  42. else {
  43. return nil
  44. }
  45. return coin.IDR! / coin.goldCoin! / (diamond.IDR! / diamond.diamond!)
  46. case .diamondToCoin:
  47. guard let coin = config.first(where: { $0.IDR != nil && $0.goldCoin != nil }),
  48. let diamond = config.first(where: { $0.IDR != nil && $0.diamond != nil })
  49. else {
  50. return nil
  51. }
  52. return diamond.IDR! / diamond.diamond! / (coin.IDR! / coin.goldCoin!)
  53. case .beanToCoin:
  54. guard let bean = config.first(where: { $0.IDR != nil && $0.bean != nil }),
  55. let coin = config.first(where: { $0.IDR != nil && $0.goldCoin != nil })
  56. else {
  57. return nil
  58. }
  59. return bean.IDR! / bean.bean! / (coin.IDR! / coin.goldCoin!)
  60. case .beanToDiamond:
  61. guard let bean = config.first(where: { $0.IDR != nil && $0.bean != nil }),
  62. let diamond = config.first(where: { $0.IDR != nil && $0.diamond != nil })
  63. else {
  64. return nil
  65. }
  66. return bean.IDR! / bean.bean! / (diamond.IDR! / diamond.diamond!)
  67. }
  68. }
  69. }
  70. class LNExchangePanel: LNPopupView {
  71. private let exchangeType: LNExchangeType
  72. private let exchangeView = UIView()
  73. private let remainLabel = UILabel()
  74. private let exchangeInput = UITextField()
  75. private let expectLabel = UILabel()
  76. private let exchangeButton = UIButton()
  77. private var delayTaskId: String?
  78. init(exchangeType: LNExchangeType) {
  79. self.exchangeType = exchangeType
  80. super.init(frame: .zero)
  81. setupViews()
  82. }
  83. required init?(coder: NSCoder) {
  84. fatalError("init(coder:) has not been implemented")
  85. }
  86. }
  87. extension LNExchangePanel: UITextFieldDelegate {
  88. func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  89. let currentText = textField.text ?? ""
  90. guard let range = Range(range, in: currentText) else { return false }
  91. let newText = currentText.replacingCharacters(in: range, with: string)
  92. if newText.isEmpty {
  93. return true
  94. }
  95. if newText.starts(with: "00") {
  96. return false
  97. }
  98. if newText.filter({ $0 == "." }).count > 2 {
  99. return false
  100. }
  101. guard Double(newText) != nil else {
  102. return false
  103. }
  104. return true
  105. }
  106. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  107. textField.resignFirstResponder()
  108. return true
  109. }
  110. }
  111. extension LNExchangePanel {
  112. private func checkExchangeButton() {
  113. let text = exchangeInput.text ?? ""
  114. if text.isEmpty {
  115. exchangeInput.attributedPlaceholder = .init(string: .init(key: "A00261"),
  116. attributes: [.font: UIFont.heading_h3,
  117. .foregroundColor: UIColor.text_2])
  118. } else {
  119. exchangeInput.attributedPlaceholder = nil
  120. }
  121. fetchExchangeResult()
  122. }
  123. private func fetchExchangeResult() {
  124. LNDelayTask.cancel(key: delayTaskId)
  125. exchangeButton.isEnabled = false
  126. exchangeButton.setBackgroundImage(nil, for: .normal)
  127. delayTaskId = LNDelayTask.perform(delay: 1.0) { [weak self] in
  128. guard let self else { return }
  129. let text = exchangeInput.text ?? ""
  130. let value = Double(text) ?? 0
  131. if value == 0 {
  132. expectLabel.text = "0"
  133. return
  134. }
  135. LNPurchaseManager.shared.expectResult(exchangeType: exchangeType, count: value)
  136. { [weak self] result in
  137. guard let self else { return }
  138. guard exchangeInput.text == text else { return }
  139. if let result {
  140. expectLabel.text = "\(result)"
  141. exchangeButton.isEnabled = true
  142. exchangeButton.setBackgroundImage(.primary_8, for: .normal)
  143. } else {
  144. expectLabel.text = ""
  145. }
  146. }
  147. }
  148. }
  149. private func setupViews() {
  150. let titleView = buildTitleView()
  151. container.addSubview(titleView)
  152. titleView.snp.makeConstraints { make in
  153. make.horizontalEdges.equalToSuperview()
  154. make.top.equalToSuperview()
  155. }
  156. let remainView = buildRemain()
  157. container.addSubview(remainView)
  158. remainView.snp.makeConstraints { make in
  159. make.horizontalEdges.equalToSuperview()
  160. make.top.equalTo(titleView.snp.bottom).offset(4)
  161. }
  162. exchangeView.backgroundColor = .fill_1
  163. exchangeView.layer.cornerRadius = 12
  164. container.addSubview(exchangeView)
  165. exchangeView.snp.makeConstraints { make in
  166. make.horizontalEdges.equalToSuperview().inset(16)
  167. make.top.equalTo(remainView.snp.bottom).offset(4)
  168. }
  169. let diamondExchange = buildExchangeView()
  170. exchangeView.addSubview(diamondExchange)
  171. diamondExchange.snp.makeConstraints { make in
  172. make.horizontalEdges.equalToSuperview()
  173. make.top.equalToSuperview()
  174. }
  175. let line = UIView()
  176. line.backgroundColor = .fill_3
  177. exchangeView.addSubview(line)
  178. line.snp.makeConstraints { make in
  179. make.horizontalEdges.equalToSuperview().inset(10)
  180. make.top.equalTo(diamondExchange.snp.bottom).offset(4)
  181. make.height.equalTo(1)
  182. }
  183. let coinView = buildExpectView()
  184. exchangeView.addSubview(coinView)
  185. coinView.snp.makeConstraints { make in
  186. make.horizontalEdges.equalToSuperview()
  187. make.bottom.equalToSuperview()
  188. make.top.equalTo(line.snp.bottom).offset(4)
  189. }
  190. exchangeButton.setTitle(.init(key: "A00262"), for: .normal)
  191. exchangeButton.setTitleColor(.text_1, for: .normal)
  192. exchangeButton.titleLabel?.font = .heading_h3
  193. exchangeButton.backgroundColor = .text_2
  194. exchangeButton.layer.cornerRadius = 23.5
  195. exchangeButton.clipsToBounds = true
  196. exchangeButton.isEnabled = false
  197. exchangeButton.setBackgroundImage(nil, for: .normal)
  198. exchangeButton.addAction(UIAction(handler: { [weak self] _ in
  199. guard let self else { return }
  200. guard let amout = Double(exchangeInput.text ?? "") else { return }
  201. guard amout > 0 else { return }
  202. LNPurchaseManager.shared.exchangeCurrency(exchangeType: exchangeType, amount: amout) { [weak self] success in
  203. guard let self else { return }
  204. guard success else { return }
  205. dismiss()
  206. showToast(.init(key: "A00263"))
  207. }
  208. }), for: .touchUpInside)
  209. container.addSubview(exchangeButton)
  210. exchangeButton.snp.makeConstraints { make in
  211. make.horizontalEdges.equalToSuperview().inset(16)
  212. make.height.equalTo(47)
  213. make.top.equalTo(exchangeView.snp.bottom).offset(24)
  214. make.bottom.equalToSuperview().offset(commonBottomInset)
  215. }
  216. }
  217. private func buildTitleView() -> UIView {
  218. let titleView = UIView()
  219. titleView.isUserInteractionEnabled = false
  220. titleView.snp.makeConstraints { make in
  221. make.height.equalTo(50)
  222. }
  223. let titleLabel = UILabel()
  224. titleLabel.text = switch exchangeType {
  225. case .coinToDiamond, .beanToDiamond: .init(key: "A00264")
  226. case .diamondToCoin, .beanToCoin: .init(key: "A00273")
  227. }
  228. titleLabel.font = .heading_h3
  229. titleLabel.textColor = .text_5
  230. titleView.addSubview(titleLabel)
  231. titleLabel.snp.makeConstraints { make in
  232. make.leading.equalToSuperview().offset(16)
  233. make.centerY.equalToSuperview()
  234. }
  235. return titleView
  236. }
  237. private func buildRemain() -> UIView {
  238. let container = UIView()
  239. container.onTap { [weak self] in
  240. guard let self else { return }
  241. endEditing(true)
  242. }
  243. container.snp.makeConstraints { make in
  244. make.height.equalTo(36)
  245. }
  246. let exchange = UIButton()
  247. exchange.setTitle(.init(key: "A00265"), for: .normal)
  248. exchange.setTitleColor(.text_6, for: .normal)
  249. exchange.titleLabel?.font = .body_xs
  250. exchange.addAction(UIAction(handler: { [weak self] _ in
  251. guard let self else { return }
  252. exchangeInput.text = switch exchangeType {
  253. case .coinToDiamond: myWalletInfo.coin.toDisplay
  254. case .diamondToCoin: myWalletInfo.diamond.toDisplay
  255. case .beanToCoin, .beanToDiamond: myWalletInfo.availableBean.toDisplay
  256. }
  257. checkExchangeButton()
  258. }), for: .touchUpInside)
  259. container.addSubview(exchange)
  260. exchange.snp.makeConstraints { make in
  261. make.centerY.equalToSuperview()
  262. make.trailing.equalToSuperview().offset(-16)
  263. }
  264. let currencyIcon: UIImageView = switch exchangeType {
  265. case .coinToDiamond: .coinImageView()
  266. case .diamondToCoin: .diamondImageView()
  267. case .beanToCoin, .beanToDiamond: .beanImageView()
  268. }
  269. container.addSubview(currencyIcon)
  270. currencyIcon.snp.makeConstraints { make in
  271. make.leading.equalToSuperview().offset(16)
  272. make.centerY.equalToSuperview()
  273. make.width.height.equalTo(24)
  274. }
  275. remainLabel.text = switch exchangeType {
  276. case .coinToDiamond: myWalletInfo.coin.toDisplay
  277. case .diamondToCoin: myWalletInfo.diamond.toDisplay
  278. case .beanToCoin, .beanToDiamond: myWalletInfo.availableBean.toDisplay
  279. }
  280. remainLabel.font = .heading_h1
  281. remainLabel.textColor = .text_5
  282. container.addSubview(remainLabel)
  283. remainLabel.snp.makeConstraints { make in
  284. make.leading.equalTo(currencyIcon.snp.trailing).offset(2)
  285. make.centerY.equalToSuperview()
  286. }
  287. return container
  288. }
  289. private func buildExchangeView() -> UIView {
  290. let container = UIView()
  291. container.onTap { [weak self] in
  292. guard let self else { return }
  293. endEditing(true)
  294. }
  295. container.snp.makeConstraints { make in
  296. make.height.equalTo(67)
  297. }
  298. let titleLabel = UILabel()
  299. titleLabel.text = switch exchangeType {
  300. case .coinToDiamond: .init(key: "A00266")
  301. case .diamondToCoin: .init(key: "A00274")
  302. case .beanToCoin, .beanToDiamond: .init(key: "B00106")
  303. }
  304. titleLabel.font = .heading_h4
  305. titleLabel.textColor = .text_5
  306. container.addSubview(titleLabel)
  307. titleLabel.snp.makeConstraints { make in
  308. make.leading.equalToSuperview().offset(12)
  309. make.centerY.equalToSuperview()
  310. }
  311. exchangeInput.font = .heading_h2
  312. exchangeInput.textColor = .text_5
  313. exchangeInput.visibleView = exchangeView
  314. exchangeInput.delegate = self
  315. exchangeInput.returnKeyType = .done
  316. exchangeInput.attributedPlaceholder = .init(
  317. string: .init(key: "A00261"),
  318. attributes: [.font: UIFont.heading_h3,
  319. .foregroundColor: UIColor.text_2])
  320. exchangeInput.keyboardType = .decimalPad
  321. exchangeInput.addAction(UIAction(handler: { [weak self] _ in
  322. guard let self else { return }
  323. checkExchangeButton()
  324. }), for: .editingChanged)
  325. container.addSubview(exchangeInput)
  326. exchangeInput.snp.makeConstraints { make in
  327. make.trailing.equalToSuperview().offset(-12)
  328. make.centerY.equalToSuperview()
  329. }
  330. let expectIcon: UIImageView = switch exchangeType {
  331. case .coinToDiamond: .coinImageView()
  332. case .diamondToCoin: .diamondImageView()
  333. case .beanToCoin, .beanToDiamond: .beanImageView()
  334. }
  335. container.addSubview(expectIcon)
  336. expectIcon.snp.makeConstraints { make in
  337. make.trailing.equalTo(exchangeInput.snp.leading).offset(-2)
  338. make.centerY.equalToSuperview()
  339. make.width.height.equalTo(20)
  340. }
  341. return container
  342. }
  343. private func buildExpectView() -> UIView {
  344. let container = UIView()
  345. container.snp.makeConstraints { make in
  346. make.height.equalTo(67)
  347. }
  348. let titleLabel = UILabel()
  349. titleLabel.text = switch exchangeType {
  350. case .coinToDiamond, .beanToDiamond: .init(key: "A00267")
  351. case .diamondToCoin, .beanToCoin: .init(key: "A00275")
  352. }
  353. titleLabel.font = .heading_h4
  354. titleLabel.textColor = .text_5
  355. container.addSubview(titleLabel)
  356. titleLabel.snp.makeConstraints { make in
  357. make.leading.equalToSuperview().offset(12)
  358. make.centerY.equalToSuperview()
  359. }
  360. expectLabel.text = "0"
  361. expectLabel.font = .heading_h2
  362. expectLabel.textColor = .text_5
  363. container.addSubview(expectLabel)
  364. expectLabel.snp.makeConstraints { make in
  365. make.trailing.equalToSuperview().offset(-12)
  366. make.centerY.equalToSuperview()
  367. }
  368. let expectIcon: UIImageView = switch exchangeType {
  369. case .coinToDiamond, .beanToDiamond: .diamondImageView()
  370. case .diamondToCoin, .beanToCoin: .coinImageView()
  371. }
  372. container.addSubview(expectIcon)
  373. expectIcon.snp.makeConstraints { make in
  374. make.trailing.equalTo(expectLabel.snp.leading).offset(-2)
  375. make.centerY.equalToSuperview()
  376. make.width.height.equalTo(20)
  377. }
  378. return container
  379. }
  380. }