123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- //
- // STPAddressViewModel.swift
- // StripeiOS
- //
- // Created by Jack Flintermann on 4/21/16.
- // Copyright © 2016 Stripe, Inc. All rights reserved.
- //
- import Contacts
- import CoreLocation
- @_spi(STP) import StripeCore
- @_spi(STP) import StripePaymentsUI
- import UIKit
- protocol STPAddressViewModelDelegate: AnyObject {
- func addressViewModelDidChange(_ addressViewModel: STPAddressViewModel)
- func addressViewModel(_ addressViewModel: STPAddressViewModel, addedCellAt index: Int)
- func addressViewModel(_ addressViewModel: STPAddressViewModel, removedCellAt index: Int)
- func addressViewModelWillUpdate(_ addressViewModel: STPAddressViewModel)
- func addressViewModelDidUpdate(_ addressViewModel: STPAddressViewModel)
- }
- class STPAddressViewModel: STPAddressFieldTableViewCellDelegate {
- private(set) var addressCells: [STPAddressFieldTableViewCell] = []
- weak var delegate: STPAddressViewModelDelegate?
- var addressFieldTableViewCountryCode: String? = Locale.autoupdatingCurrent.regionCode {
- didSet {
- updatePostalCodeCellIfNecessary()
- if let addressFieldTableViewCountryCode = addressFieldTableViewCountryCode {
- for cell in addressCells {
- cell.delegateCountryCodeDidChange(countryCode: addressFieldTableViewCountryCode)
- }
- }
- }
- }
- var address: STPAddress {
- get {
- let address = STPAddress()
- for cell in addressCells {
- switch cell.type {
- case .name:
- address.name = cell.contents
- case .line1:
- address.line1 = cell.contents
- case .line2:
- address.line2 = cell.contents
- case .city:
- address.city = cell.contents
- case .state:
- address.state = cell.contents
- case .zip:
- address.postalCode = cell.contents
- case .country:
- address.country = cell.contents
- case .email:
- address.email = cell.contents
- case .phone:
- address.phone = cell.contents
- }
- }
- // Prefer to use the contents of STPAddressFieldTypeCountry, but fallback to
- // `addressFieldTableViewCountryCode` if nil (important for STPBillingAddressFieldsPostalCode)
- address.country = address.country ?? addressFieldTableViewCountryCode
- return address
- }
- set(address) {
- if let country = address.country {
- addressFieldTableViewCountryCode = country
- }
- for cell in addressCells {
- switch cell.type {
- case .name:
- cell.contents = address.name
- case .line1:
- cell.contents = address.line1
- case .line2:
- cell.contents = address.line2
- case .city:
- cell.contents = address.city
- case .state:
- cell.contents = address.state
- case .zip:
- cell.contents = address.postalCode
- case .country:
- cell.contents = address.country
- case .email:
- cell.contents = address.email
- case .phone:
- cell.contents = address.phone
- }
- }
- }
- }
- // The default value of availableCountries is nil, which will allow all known countries.
- var availableCountries: Set<String>?
- var isValid: Bool {
- if isBillingAddress {
- // The AddressViewModel is only for address fields.
- // Determining whether the postal code is present is up to the
- // STPCardTextFieldViewModel.
- if requiredBillingAddressFields == .postalCode {
- return true
- } else {
- return address.containsRequiredFields(requiredBillingAddressFields)
- }
- } else {
- if let requiredShippingAddressFields = requiredShippingAddressFields {
- return address.containsRequiredShippingAddressFields(requiredShippingAddressFields)
- }
- return false
- }
- }
- // The default value of availableCountries is nil, which will allow all known countries.
- init(
- requiredBillingFields requiredBillingAddressFields: STPBillingAddressFields,
- availableCountries: Set<String>? = nil
- ) {
- isBillingAddress = true
- self.availableCountries = availableCountries
- self.requiredBillingAddressFields = requiredBillingAddressFields
- switch requiredBillingAddressFields {
- case .none:
- addressCells = []
- case .zip, .postalCode:
- addressCells = [] // Postal code cell will be added later if necessary
- case .full:
- addressCells = [
- STPAddressFieldTableViewCell(
- type: .name,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .line1,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .line2,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .country,
- contents: addressFieldTableViewCountryCode,
- lastInList: false,
- delegate: self
- ),
- // Postal code cell will be added here later if necessary
- STPAddressFieldTableViewCell(
- type: .city,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .state,
- contents: "",
- lastInList: true,
- delegate: self
- ),
- ]
- case .name:
- addressCells = [
- STPAddressFieldTableViewCell(
- type: .name,
- contents: "",
- lastInList: true,
- delegate: self
- ),
- ]
- default:
- fatalError()
- }
- commonInit()
- }
- init(
- requiredShippingFields requiredShippingAddressFields: Set<STPContactField>,
- availableCountries: Set<String>? = nil
- ) {
- isBillingAddress = false
- self.availableCountries = availableCountries
- self.requiredShippingAddressFields = requiredShippingAddressFields
- var cells: [STPAddressFieldTableViewCell] = []
- if requiredShippingAddressFields.contains(STPContactField.name) {
- cells.append(
- STPAddressFieldTableViewCell(
- type: .name,
- contents: "",
- lastInList: false,
- delegate: self
- )
- )
- }
- if requiredShippingAddressFields.contains(.emailAddress) {
- cells.append(
- STPAddressFieldTableViewCell(
- type: .email,
- contents: "",
- lastInList: false,
- delegate: self
- )
- )
- }
- if requiredShippingAddressFields.contains(STPContactField.postalAddress) {
- var postalCells = [
- STPAddressFieldTableViewCell(
- type: .name,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .line1,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .line2,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .country,
- contents: addressFieldTableViewCountryCode,
- lastInList: false,
- delegate: self
- ),
- // Postal code cell will be added here later if necessary
- STPAddressFieldTableViewCell(
- type: .city,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- STPAddressFieldTableViewCell(
- type: .state,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- ]
- if requiredShippingAddressFields.contains(.name) {
- postalCells.remove(at: 0)
- }
- cells.append(contentsOf: postalCells.compactMap { $0 })
- }
- if requiredShippingAddressFields.contains(.phoneNumber) {
- cells.append(
- STPAddressFieldTableViewCell(
- type: .phone,
- contents: "",
- lastInList: false,
- delegate: self
- )
- )
- }
- if let lastCell = cells.last {
- lastCell.lastInList = true
- }
- addressCells = cells
- commonInit()
- }
- private func cell(at index: Int) -> STPAddressFieldTableViewCell? {
- guard index > 0,
- index < addressCells.count
- else {
- return nil
- }
- return addressCells[index]
- }
- private var isBillingAddress = false
- private var requiredBillingAddressFields: STPBillingAddressFields = .none
- private var requiredShippingAddressFields: Set<STPContactField>?
- private var showingPostalCodeCell = false
- private var geocodeInProgress = false
- private func commonInit() {
- if let countryCode = Locale.autoupdatingCurrent.regionCode {
- addressFieldTableViewCountryCode = countryCode
- }
- updatePostalCodeCellIfNecessary()
- }
- private func updatePostalCodeCellIfNecessary() {
- delegate?.addressViewModelWillUpdate(self)
- let shouldBeShowingPostalCode = STPPostalCodeValidator.postalCodeIsRequired(
- forCountryCode: addressFieldTableViewCountryCode
- )
- if shouldBeShowingPostalCode && !showingPostalCodeCell {
- if containsStateAndPostalFields() {
- // Add before city
- let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .city }) ?? 0
- var mutableAddressCells = addressCells
- mutableAddressCells.insert(
- STPAddressFieldTableViewCell(
- type: .zip,
- contents: "",
- lastInList: false,
- delegate: self
- ),
- at: zipFieldIndex
- )
- addressCells = mutableAddressCells
- delegate?.addressViewModel(self, addedCellAt: zipFieldIndex)
- delegate?.addressViewModelDidChange(self)
- }
- } else if !shouldBeShowingPostalCode && showingPostalCodeCell {
- if containsStateAndPostalFields() {
- if let zipFieldIndex = addressCells.firstIndex(where: { $0.type == .zip }) {
- var mutableAddressCells = addressCells
- mutableAddressCells.remove(at: zipFieldIndex)
- addressCells = mutableAddressCells
- delegate?.addressViewModel(self, removedCellAt: zipFieldIndex)
- delegate?.addressViewModelDidChange(self)
- }
- }
- }
- showingPostalCodeCell = shouldBeShowingPostalCode
- delegate?.addressViewModelDidUpdate(self)
- }
- private func containsStateAndPostalFields() -> Bool {
- if isBillingAddress {
- return requiredBillingAddressFields == .full
- } else {
- return requiredShippingAddressFields?.contains(.postalAddress) ?? false
- }
- }
- func updateCityAndState(fromZipCodeCell zipCell: STPAddressFieldTableViewCell?) {
- let zipCode = zipCell?.contents
- if geocodeInProgress || zipCode == nil || !(zipCell?.textField.validText ?? false)
- || !(addressFieldTableViewCountryCode == "US")
- {
- return
- }
- var cityCell: STPAddressFieldTableViewCell?
- var stateCell: STPAddressFieldTableViewCell?
- for cell in addressCells {
- if cell.type == .city {
- cityCell = cell
- } else if cell.type == .state {
- stateCell = cell
- }
- }
- if (cityCell == nil && stateCell == nil)
- || ((cityCell?.contents?.count ?? 0) > 0 || (stateCell?.contents?.count ?? 0) > 0)
- {
- // Don't auto fill if either have text already
- // Or if neither are non-nil
- return
- } else {
- geocodeInProgress = true
- let geocoder = CLGeocoder()
- let onCompletion: CLGeocodeCompletionHandler = { placemarks, error in
- stpDispatchToMainThreadIfNecessary({
- if (placemarks?.count ?? 0) > 0 && error == nil {
- let placemark = placemarks?.first
- if (cityCell?.contents?.count ?? 0) == 0
- && (stateCell?.contents?.count ?? 0) == 0
- && (zipCell?.contents == zipCode)
- {
- // Check contents again to make sure they're still empty
- // And that zipcode hasn't changed to something else
- cityCell?.contents = placemark?.locality
- stateCell?.contents = placemark?.administrativeArea
- }
- }
- self.geocodeInProgress = false
- })
- }
- let address = CNMutablePostalAddress()
- address.postalCode = zipCode ?? ""
- address.isoCountryCode = addressFieldTableViewCountryCode ?? ""
- geocoder.geocodePostalAddress(
- address,
- completionHandler: onCompletion
- )
- }
- }
- private func cell(after cell: STPAddressFieldTableViewCell?) -> STPAddressFieldTableViewCell? {
- guard let cell = cell,
- let cellIndex = addressCells.firstIndex(of: cell),
- cellIndex + 1 < addressCells.count
- else {
- return nil
- }
- return addressCells[cellIndex + 1]
- }
- func addressFieldTableViewCellDidUpdateText(_ cell: STPAddressFieldTableViewCell) {
- delegate?.addressViewModelDidChange(self)
- }
- func addressFieldTableViewCellDidReturn(_ cell: STPAddressFieldTableViewCell) {
- _ = self.cell(after: cell)?.becomeFirstResponder()
- }
- func addressFieldTableViewCellDidEndEditing(_ cell: STPAddressFieldTableViewCell) {
- if cell.type == .zip {
- updateCityAndState(fromZipCodeCell: cell)
- }
- }
- }
|