STPAddressViewModel.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. //
  2. // STPAddressViewModel.swift
  3. // StripeiOS
  4. //
  5. // Created by Jack Flintermann on 4/21/16.
  6. // Copyright © 2016 Stripe, Inc. All rights reserved.
  7. //
  8. import Contacts
  9. import CoreLocation
  10. @_spi(STP) import StripeCore
  11. @_spi(STP) import StripePaymentsUI
  12. import UIKit
  13. protocol STPAddressViewModelDelegate: AnyObject {
  14. func addressViewModelDidChange(_ addressViewModel: STPAddressViewModel)
  15. func addressViewModel(_ addressViewModel: STPAddressViewModel, addedCellAt index: Int)
  16. func addressViewModel(_ addressViewModel: STPAddressViewModel, removedCellAt index: Int)
  17. func addressViewModelWillUpdate(_ addressViewModel: STPAddressViewModel)
  18. func addressViewModelDidUpdate(_ addressViewModel: STPAddressViewModel)
  19. }
  20. class STPAddressViewModel: STPAddressFieldTableViewCellDelegate {
  21. private(set) var addressCells: [STPAddressFieldTableViewCell] = []
  22. weak var delegate: STPAddressViewModelDelegate?
  23. var addressFieldTableViewCountryCode: String? = Locale.autoupdatingCurrent.regionCode {
  24. didSet {
  25. updatePostalCodeCellIfNecessary()
  26. if let addressFieldTableViewCountryCode = addressFieldTableViewCountryCode {
  27. for cell in addressCells {
  28. cell.delegateCountryCodeDidChange(countryCode: addressFieldTableViewCountryCode)
  29. }
  30. }
  31. }
  32. }
  33. var address: STPAddress {
  34. get {
  35. let address = STPAddress()
  36. for cell in addressCells {
  37. switch cell.type {
  38. case .name:
  39. address.name = cell.contents
  40. case .line1:
  41. address.line1 = cell.contents
  42. case .line2:
  43. address.line2 = cell.contents
  44. case .city:
  45. address.city = cell.contents
  46. case .state:
  47. address.state = cell.contents
  48. case .zip:
  49. address.postalCode = cell.contents
  50. case .country:
  51. address.country = cell.contents
  52. case .email:
  53. address.email = cell.contents
  54. case .phone:
  55. address.phone = cell.contents
  56. }
  57. }
  58. // Prefer to use the contents of STPAddressFieldTypeCountry, but fallback to
  59. // `addressFieldTableViewCountryCode` if nil (important for STPBillingAddressFieldsPostalCode)
  60. address.country = address.country ?? addressFieldTableViewCountryCode
  61. return address
  62. }
  63. set(address) {
  64. if let country = address.country {
  65. addressFieldTableViewCountryCode = country
  66. }
  67. for cell in addressCells {
  68. switch cell.type {
  69. case .name:
  70. cell.contents = address.name
  71. case .line1:
  72. cell.contents = address.line1
  73. case .line2:
  74. cell.contents = address.line2
  75. case .city:
  76. cell.contents = address.city
  77. case .state:
  78. cell.contents = address.state
  79. case .zip:
  80. cell.contents = address.postalCode
  81. case .country:
  82. cell.contents = address.country
  83. case .email:
  84. cell.contents = address.email
  85. case .phone:
  86. cell.contents = address.phone
  87. }
  88. }
  89. }
  90. }
  91. // The default value of availableCountries is nil, which will allow all known countries.
  92. var availableCountries: Set<String>?
  93. var isValid: Bool {
  94. if isBillingAddress {
  95. // The AddressViewModel is only for address fields.
  96. // Determining whether the postal code is present is up to the
  97. // STPCardTextFieldViewModel.
  98. if requiredBillingAddressFields == .postalCode {
  99. return true
  100. } else {
  101. return address.containsRequiredFields(requiredBillingAddressFields)
  102. }
  103. } else {
  104. if let requiredShippingAddressFields = requiredShippingAddressFields {
  105. return address.containsRequiredShippingAddressFields(requiredShippingAddressFields)
  106. }
  107. return false
  108. }
  109. }
  110. // The default value of availableCountries is nil, which will allow all known countries.
  111. init(
  112. requiredBillingFields requiredBillingAddressFields: STPBillingAddressFields,
  113. availableCountries: Set<String>? = nil
  114. ) {
  115. isBillingAddress = true
  116. self.availableCountries = availableCountries
  117. self.requiredBillingAddressFields = requiredBillingAddressFields
  118. switch requiredBillingAddressFields {
  119. case .none:
  120. addressCells = []
  121. case .zip, .postalCode:
  122. addressCells = [] // Postal code cell will be added later if necessary
  123. case .full:
  124. addressCells = [
  125. STPAddressFieldTableViewCell(
  126. type: .name,
  127. contents: "",
  128. lastInList: false,
  129. delegate: self
  130. ),
  131. STPAddressFieldTableViewCell(
  132. type: .line1,
  133. contents: "",
  134. lastInList: false,
  135. delegate: self
  136. ),
  137. STPAddressFieldTableViewCell(
  138. type: .line2,
  139. contents: "",
  140. lastInList: false,
  141. delegate: self
  142. ),
  143. STPAddressFieldTableViewCell(
  144. type: .country,
  145. contents: addressFieldTableViewCountryCode,
  146. lastInList: false,
  147. delegate: self
  148. ),
  149. // Postal code cell will be added here later if necessary
  150. STPAddressFieldTableViewCell(
  151. type: .city,
  152. contents: "",
  153. lastInList: false,
  154. delegate: self
  155. ),
  156. STPAddressFieldTableViewCell(
  157. type: .state,
  158. contents: "",
  159. lastInList: true,
  160. delegate: self
  161. ),
  162. ]
  163. case .name:
  164. addressCells = [
  165. STPAddressFieldTableViewCell(
  166. type: .name,
  167. contents: "",
  168. lastInList: true,
  169. delegate: self
  170. ),
  171. ]
  172. default:
  173. fatalError()
  174. }
  175. commonInit()
  176. }
  177. init(
  178. requiredShippingFields requiredShippingAddressFields: Set<STPContactField>,
  179. availableCountries: Set<String>? = nil
  180. ) {
  181. isBillingAddress = false
  182. self.availableCountries = availableCountries
  183. self.requiredShippingAddressFields = requiredShippingAddressFields
  184. var cells: [STPAddressFieldTableViewCell] = []
  185. if requiredShippingAddressFields.contains(STPContactField.name) {
  186. cells.append(
  187. STPAddressFieldTableViewCell(
  188. type: .name,
  189. contents: "",
  190. lastInList: false,
  191. delegate: self
  192. )
  193. )
  194. }
  195. if requiredShippingAddressFields.contains(.emailAddress) {
  196. cells.append(
  197. STPAddressFieldTableViewCell(
  198. type: .email,
  199. contents: "",
  200. lastInList: false,
  201. delegate: self
  202. )
  203. )
  204. }
  205. if requiredShippingAddressFields.contains(STPContactField.postalAddress) {
  206. var postalCells = [
  207. STPAddressFieldTableViewCell(
  208. type: .name,
  209. contents: "",
  210. lastInList: false,
  211. delegate: self
  212. ),
  213. STPAddressFieldTableViewCell(
  214. type: .line1,
  215. contents: "",
  216. lastInList: false,
  217. delegate: self
  218. ),
  219. STPAddressFieldTableViewCell(
  220. type: .line2,
  221. contents: "",
  222. lastInList: false,
  223. delegate: self
  224. ),
  225. STPAddressFieldTableViewCell(
  226. type: .country,
  227. contents: addressFieldTableViewCountryCode,
  228. lastInList: false,
  229. delegate: self
  230. ),
  231. // Postal code cell will be added here later if necessary
  232. STPAddressFieldTableViewCell(
  233. type: .city,
  234. contents: "",
  235. lastInList: false,
  236. delegate: self
  237. ),
  238. STPAddressFieldTableViewCell(
  239. type: .state,
  240. contents: "",
  241. lastInList: false,
  242. delegate: self
  243. ),
  244. ]
  245. if requiredShippingAddressFields.contains(.name) {
  246. postalCells.remove(at: 0)
  247. }
  248. cells.append(contentsOf: postalCells.compactMap { $0 })
  249. }
  250. if requiredShippingAddressFields.contains(.phoneNumber) {
  251. cells.append(
  252. STPAddressFieldTableViewCell(
  253. type: .phone,
  254. contents: "",
  255. lastInList: false,
  256. delegate: self
  257. )
  258. )
  259. }
  260. if let lastCell = cells.last {
  261. lastCell.lastInList = true
  262. }
  263. addressCells = cells
  264. commonInit()
  265. }
  266. private func cell(at index: Int) -> STPAddressFieldTableViewCell? {
  267. guard index > 0,
  268. index < addressCells.count
  269. else {
  270. return nil
  271. }
  272. return addressCells[index]
  273. }
  274. private var isBillingAddress = false
  275. private var requiredBillingAddressFields: STPBillingAddressFields = .none
  276. private var requiredShippingAddressFields: Set<STPContactField>?
  277. private var showingPostalCodeCell = false
  278. private var geocodeInProgress = false
  279. private func commonInit() {
  280. if let countryCode = Locale.autoupdatingCurrent.regionCode {
  281. addressFieldTableViewCountryCode = countryCode
  282. }
  283. updatePostalCodeCellIfNecessary()
  284. }
  285. private func updatePostalCodeCellIfNecessary() {
  286. delegate?.addressViewModelWillUpdate(self)
  287. let shouldBeShowingPostalCode = STPPostalCodeValidator.postalCodeIsRequired(
  288. forCountryCode: addressFieldTableViewCountryCode
  289. )
  290. if shouldBeShowingPostalCode && !showingPostalCodeCell {
  291. if containsStateAndPostalFields() {
  292. // Add before city
  293. let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .city }) ?? 0
  294. var mutableAddressCells = addressCells
  295. mutableAddressCells.insert(
  296. STPAddressFieldTableViewCell(
  297. type: .zip,
  298. contents: "",
  299. lastInList: false,
  300. delegate: self
  301. ),
  302. at: zipFieldIndex
  303. )
  304. addressCells = mutableAddressCells
  305. delegate?.addressViewModel(self, addedCellAt: zipFieldIndex)
  306. delegate?.addressViewModelDidChange(self)
  307. }
  308. } else if !shouldBeShowingPostalCode && showingPostalCodeCell {
  309. if containsStateAndPostalFields() {
  310. if let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .zip }) {
  311. var mutableAddressCells = addressCells
  312. mutableAddressCells.remove(at: zipFieldIndex)
  313. addressCells = mutableAddressCells
  314. delegate?.addressViewModel(self, removedCellAt: zipFieldIndex)
  315. delegate?.addressViewModelDidChange(self)
  316. }
  317. }
  318. }
  319. showingPostalCodeCell = shouldBeShowingPostalCode
  320. delegate?.addressViewModelDidUpdate(self)
  321. }
  322. private func containsStateAndPostalFields() -> Bool {
  323. if isBillingAddress {
  324. return requiredBillingAddressFields == .full
  325. } else {
  326. return requiredShippingAddressFields?.contains(.postalAddress) ?? false
  327. }
  328. }
  329. func updateCityAndState(fromZipCodeCell zipCell: STPAddressFieldTableViewCell?) {
  330. let zipCode = zipCell?.contents
  331. if geocodeInProgress || zipCode == nil || !(zipCell?.textField.validText ?? false)
  332. || !(addressFieldTableViewCountryCode == "US")
  333. {
  334. return
  335. }
  336. var cityCell: STPAddressFieldTableViewCell?
  337. var stateCell: STPAddressFieldTableViewCell?
  338. for cell in addressCells {
  339. if cell.type == .city {
  340. cityCell = cell
  341. } else if cell.type == .state {
  342. stateCell = cell
  343. }
  344. }
  345. if (cityCell == nil && stateCell == nil)
  346. || ((cityCell?.contents?.count ?? 0) > 0 || (stateCell?.contents?.count ?? 0) > 0)
  347. {
  348. // Don't auto fill if either have text already
  349. // Or if neither are non-nil
  350. return
  351. } else {
  352. geocodeInProgress = true
  353. let geocoder = CLGeocoder()
  354. let onCompletion: CLGeocodeCompletionHandler = { placemarks, error in
  355. stpDispatchToMainThreadIfNecessary({
  356. if (placemarks?.count ?? 0) > 0 && error == nil {
  357. let placemark = placemarks?.first
  358. if (cityCell?.contents?.count ?? 0) == 0
  359. && (stateCell?.contents?.count ?? 0) == 0
  360. && (zipCell?.contents == zipCode)
  361. {
  362. // Check contents again to make sure they're still empty
  363. // And that zipcode hasn't changed to something else
  364. cityCell?.contents = placemark?.locality
  365. stateCell?.contents = placemark?.administrativeArea
  366. }
  367. }
  368. self.geocodeInProgress = false
  369. })
  370. }
  371. let address = CNMutablePostalAddress()
  372. address.postalCode = zipCode ?? ""
  373. address.isoCountryCode = addressFieldTableViewCountryCode ?? ""
  374. geocoder.geocodePostalAddress(
  375. address,
  376. completionHandler: onCompletion
  377. )
  378. }
  379. }
  380. private func cell(after cell: STPAddressFieldTableViewCell?) -> STPAddressFieldTableViewCell? {
  381. guard let cell = cell,
  382. let cellIndex = addressCells.firstIndex(of: cell),
  383. cellIndex + 1 < addressCells.count
  384. else {
  385. return nil
  386. }
  387. return addressCells[cellIndex + 1]
  388. }
  389. func addressFieldTableViewCellDidUpdateText(_ cell: STPAddressFieldTableViewCell) {
  390. delegate?.addressViewModelDidChange(self)
  391. }
  392. func addressFieldTableViewCellDidReturn(_ cell: STPAddressFieldTableViewCell) {
  393. _ = self.cell(after: cell)?.becomeFirstResponder()
  394. }
  395. func addressFieldTableViewCellDidEndEditing(_ cell: STPAddressFieldTableViewCell) {
  396. if cell.type == .zip {
  397. updateCityAndState(fromZipCodeCell: cell)
  398. }
  399. }
  400. }