diff --git a/Kiosk.xcodeproj/project.pbxproj b/Kiosk.xcodeproj/project.pbxproj index be5d3d94..02c48fb2 100644 --- a/Kiosk.xcodeproj/project.pbxproj +++ b/Kiosk.xcodeproj/project.pbxproj @@ -1151,6 +1151,7 @@ "${BUILT_PRODUCTS_DIR}/Artsy+UIFonts/Artsy_UIFonts.framework", "${BUILT_PRODUCTS_DIR}/Artsy+UILabels/Artsy_UILabels.framework", "${BUILT_PRODUCTS_DIR}/Artsy-UIButtons/Artsy_UIButtons.framework", + "${PODS_ROOT}/CardFlight-v4/CardFlight.framework", "${BUILT_PRODUCTS_DIR}/DZNWebViewController/DZNWebViewController.framework", "${BUILT_PRODUCTS_DIR}/ECPhoneNumberFormatter/ECPhoneNumberFormatter.framework", "${BUILT_PRODUCTS_DIR}/FLKAutoLayout/FLKAutoLayout.framework", @@ -1186,6 +1187,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Artsy_UIFonts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Artsy_UILabels.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Artsy_UIButtons.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CardFlight.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNWebViewController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ECPhoneNumberFormatter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FLKAutoLayout.framework", @@ -1551,6 +1553,7 @@ "$(SRCROOT)/Pods/CardFlight/**", ); INFOPLIST_FILE = "Kiosk/Supporting Files/Info.plist"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = net.artsy.kiosk.beta; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Kiosk/Supporting Files/BridgingHeader.h"; diff --git a/Kiosk/Admin/AdminCardTestingViewController.swift b/Kiosk/Admin/AdminCardTestingViewController.swift index 4a2b169b..d4d50986 100644 --- a/Kiosk/Admin/AdminCardTestingViewController.swift +++ b/Kiosk/Admin/AdminCardTestingViewController.swift @@ -38,14 +38,11 @@ class AdminCardTestingViewController: UIViewController { return } - let cardDetails = "Card: \(card.name ?? "") - \(card.last4 ?? "") \n \(card.cardToken ?? "")" + let cardDetails = "Card: \(card.cardInfo.cardholderName ?? "") - \(card.cardInfo.lastFour ?? "") \n \(card.token)" self.log(cardDetails) } } .disposed(by: rx.disposeBag) - - - cardHandler.startSearching() } override func viewWillDisappear(_ animated: Bool) { @@ -53,6 +50,11 @@ class AdminCardTestingViewController: UIViewController { cardHandler.end() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + cardHandler.startSearching() + } + func log(_ string: String) { self.logTextView.text = "\(self.logTextView.text ?? "")\n\(string)" diff --git a/Kiosk/App/CardHandler.swift b/Kiosk/App/CardHandler.swift index 6dba935c..cdf814b7 100644 --- a/Kiosk/App/CardHandler.swift +++ b/Kiosk/App/CardHandler.swift @@ -1,107 +1,181 @@ import UIKit import RxSwift +import CardFlight -class CardHandler: NSObject, CFTReaderDelegate { +class CardHandler: NSObject, CFTTransactionDelegate { - fileprivate let _cardStatus = PublishSubject() + private let _cardStatus = PublishSubject() + private let _userMessages = PublishSubject() + private var cardReader: CFTCardReaderInfo? + + var transaction: CFTTransaction? var cardStatus: Observable { return _cardStatus.asObservable() } - var card: CFTCard? - + var userMessages: Observable { + // User messages are things like "Swipe card", "processing", or "Swipe card again". Due to a problem with the + // CardFlight SDK, the user is prompted to accept processing for card tokenization, which is provides a + // unfriendly user experience (prompting to accept a transaction that we're not actually placing). So we + // auto-accept these requests and filter out confirmation messages, which don't apply to tokenization flows, + // until this issue is fixed: https://github.com/CardFlight/cardflight-v4-ios/issues/4 + return _userMessages + .asObservable() + .filter { message -> Bool in + !message.hasSuffix("?") + } + } + + var cardFlightCredentials: CFTCredentials { + let credentials = CFTCredentials() + credentials.setup(apiKey: self.APIKey, accountToken: self.APIToken, completion: nil) + return credentials + } + + var card: (cardInfo: CFTCardInfo, token: String)? + + let APIKey: String let APIToken: String - var reader: CFTReader! - lazy var sessionManager = CFTSessionManager.sharedInstance()! - init(apiKey: String, accountToken: String){ APIKey = apiKey APIToken = accountToken super.init() - sessionManager.setApiToken(APIKey, accountToken: APIToken, completed: nil) + self.transaction = CFTTransaction(delegate: self) } - func startSearching() { - sessionManager.setLogging(true) + deinit { + self.end() + } - reader = CFTReader(reader: CFTReaderType.UNKNOWN) - reader.delegate = self - reader.swipeHasTimeout(false) - _cardStatus.onNext("Started searching") + func startSearching() { + _cardStatus.onNext("Starting search...") + let tokenizationParameters = CFTTokenizationParameters(customerId: nil, credentials: self.cardFlightCredentials) + self.transaction?.beginTokenizing(tokenizationParameters: tokenizationParameters) } func end() { - reader.cancelTransaction() - reader = nil + transaction?.select(processOption: CFTProcessOption.abort) + transaction = nil } - func readerCardResponse(_ card: CFTCard?, withError error: Error?) { - if let card = card { - self.card = card; - _cardStatus.onNext("Got Card") - - card.tokenizeCard(success: { [weak self] in - self?._cardStatus.onCompleted() - logger.log("Card was tokenized") - - }, failure: { [weak self] (error) in - self?._cardStatus.onNext("Card Flight Error: \(String(describing: error))"); - logger.log("Card was not tokenizable") - }) - - } else if let error = error { - self._cardStatus.onNext("response Error \(error)"); - logger.log("CardReader got a response it cannot handle") - - - reader.beginSwipe(); + func transaction(_ transaction: CFTTransaction, didUpdate state: CFTTransactionState, error: Error?) { + switch state { + case .completed: + _cardStatus.onNext("Transaction completed") + case .processing: + _cardStatus.onNext("Transaction processing") + case .deferred: + _cardStatus.onNext("Transaction deferred") + case .pendingCardInput: + _cardStatus.onNext("Pending card input") + transaction.select(cardReaderInfo: cardReader, cardReaderModel: cardReader?.cardReaderModel ?? .unknown) + case .pendingTransactionParameters: + _cardStatus.onNext("Pending transaction parameters") + case .unknown: + _cardStatus.onNext("Unknown transactionstate") + case .pendingProcessOption: + break } } - func transactionResult(_ charge: CFTCharge!, withError error: Error!) { - logger.log("Unexcepted call to transactionResult callback: \(charge)\n\(error)") + func transaction(_ transaction: CFTTransaction, didComplete historicalTransaction: CFTHistoricalTransaction) { + if let cardInfo = historicalTransaction.cardInfo, let token = historicalTransaction.cardToken { + self.card = (cardInfo: cardInfo, token: token) + _cardStatus.onNext("Got Card") + _cardStatus.onCompleted() + } else { + _cardStatus.onNext("Card Flight Error – could not retrieve card data."); + if let error = historicalTransaction.error { + _cardStatus.onNext("response Error \(error)"); + logger.log("CardReader got a response it cannot handle") + } + startSearching() + } } - // handle other delegate call backs with the status messages + func transaction(_ transaction: CFTTransaction, didReceive cardReaderEvent: CFTCardReaderEvent, cardReaderInfo: CFTCardReaderInfo?) { + _cardStatus.onNext(cardReaderEvent.statusMessage) + } - func readerIsAttached() { - _cardStatus.onNext("Reader is attatched"); + func transaction(_ transaction: CFTTransaction, didUpdate cardReaderArray: [CFTCardReaderInfo]) { + self.cardReader = cardReaderArray.first + _cardStatus.onNext("Received new card reader availability, number of readers: \(cardReaderArray.count)") } - func readerIsConnecting() { - _cardStatus.onNext("Reader is connecting"); + func transaction(_ transaction: CFTTransaction, didRequestProcessOption cardInfo: CFTCardInfo) { + logger.log("Received request for processing option, will process transaction.") + _cardStatus.onNext("Request for process option, automatically processing...") + // We auto-accept the process option on the user's behalf because the prompt doesn't make sense in a + // tokenization flow. See comments in `userMessages` property above. + transaction.select(processOption: .process) } - func readerIsDisconnected() { - _cardStatus.onNext("Reader is disconnected"); - logger.log("Card Reader Disconnected") + func transaction(_ transaction: CFTTransaction, didRequestDisplay message: CFTMessage) { + let message = message.primary ?? message.secondary ?? "" + _userMessages.onNext(message) + logger.log("Received request to display message: \(message)") + _cardStatus.onNext("Received message for user: \(message)") } +} - func readerSwipeDidCancel() { - _cardStatus.onNext("Reader did cancel"); - logger.log("Card Reader was Cancelled") +typealias UnhandledDelegateCallbacks = CardHandler +/// We don't expect any of these functions to be called, but they are required for the delegate protocol. +extension UnhandledDelegateCallbacks { + func transaction(_ transaction: CFTTransaction, didDefer transactionData: Data) { + logger.log("Transaction has been deferred.") + _cardStatus.onNext("Transaction deferred") } - func readerGenericResponse(_ cardData: String!) { - _cardStatus.onNext("Reader received non-card data: \(cardData ?? "") "); - reader.beginSwipe() + public func transaction(_ transaction: CFTTransaction, didRequest cvm: CFTCVM) { + if cvm == CFTCVM.signature { + logger.log("Transaction requested signature from user, which should not occur for tokenization.") + _cardStatus.onNext("Ignoring user signature request from CardFlight") + } } +} - func readerIsConnected(_ isConnected: Bool, withError error: Error!) { - if isConnected { - _cardStatus.onNext("Reader is connected") - reader.beginSwipe() - } else { - if (error != nil) { - _cardStatus.onNext("Reader is disconnected: \(error.localizedDescription)"); - } else { - _cardStatus.onNext("Reader is disconnected"); - } +extension CFTCardReaderEvent { + var statusMessage: String { + switch self { + case .unknown: + return "Unknown card event" + case .disconnected: + return "Reader is disconnected" + case .connected: + return "Reader is connected" + case .connectionErrored: + return "Connection error occurred" + case .cardSwiped: + return "Card swiped" + case .cardSwipeErrored: + return "Card swipe error" + case .cardInserted: + return "Card inserted" + case .cardInsertErrored: + return "Card insertion error" + case .cardRemoved: + return "Card removed" + case .cardTapped: + return "Card tapped" + case .cardTapErrored: + return "Card tap error" + case .updateStarted: + return "Update started" + case .updateCompleted: + return "Updated completed" + case .audioRecordingPermissionNotGranted: + return "iOS audio permissions no granted" + case .fatalError: + return "Fatal error" + case .connecting: + return "Connecting" + case .batteryStatusUpdated: + return "Battery status updated" } } } diff --git a/Kiosk/App/GlobalFunctions.swift b/Kiosk/App/GlobalFunctions.swift index ef76f695..298e237a 100644 --- a/Kiosk/App/GlobalFunctions.swift +++ b/Kiosk/App/GlobalFunctions.swift @@ -36,7 +36,7 @@ func responseIsOK(_ response: Response) -> Bool { func detectDevelopmentEnvironment() -> Bool { var developmentEnvironment = false - #if DEBUG || (arch(i386) || arch(x86_64)) && os(iOS) + #if DEBUG || (arch(i386) || arch(x86_64)) developmentEnvironment = true #endif return developmentEnvironment diff --git a/Kiosk/Bid Fulfillment/SwipeCreditCardViewController.swift b/Kiosk/Bid Fulfillment/SwipeCreditCardViewController.swift index edda3582..cdf7f03c 100644 --- a/Kiosk/Bid Fulfillment/SwipeCreditCardViewController.swift +++ b/Kiosk/Bid Fulfillment/SwipeCreditCardViewController.swift @@ -9,11 +9,10 @@ class SwipeCreditCardViewController: UIViewController, RegistrationSubController @IBOutlet var cardStatusLabel: ARSerifLabel! let finished = PublishSubject() + @IBOutlet weak var titleLabel: ARSerifLabel! @IBOutlet weak var spinner: Spinner! - @IBOutlet weak var processingLabel: UILabel! @IBOutlet weak var illustrationImageView: UIImageView! - @IBOutlet weak var titleLabel: ARSerifLabel! class func instantiateFromStoryboard(_ storyboard: UIStoryboard) -> SwipeCreditCardViewController { return storyboard.viewController(withID: .RegisterCreditCard) as! SwipeCreditCardViewController @@ -44,6 +43,20 @@ class SwipeCreditCardViewController: UIViewController, RegistrationSubController super.viewDidLoad() self.setInProgress(false) + let pleaseWaitMessage = "Please wait..." + + cardHandler.userMessages + .startWith(pleaseWaitMessage) + .map { message in + if message.isEmpty { + return pleaseWaitMessage + } else { + return message + } + } + .bind(to: titleLabel.rx.text) + .disposed(by: rx.disposeBag) + cardHandler.cardStatus .takeUntil(self.viewWillDisappear) .subscribe(onNext: { message in @@ -51,10 +64,6 @@ class SwipeCreditCardViewController: UIViewController, RegistrationSubController if message == "Got Card" { self.setInProgress(true) } - - if message.hasPrefix("Card Flight Error") { - self.processingLabel.text = "ERROR PROCESSING CARD - SEE ADMIN" - } }, onError: { error in self.cardStatusLabel.text = "Card Status: Errored" @@ -66,13 +75,13 @@ class SwipeCreditCardViewController: UIViewController, RegistrationSubController self.cardStatusLabel.text = "Card Status: completed" if let card = self.cardHandler.card { - self.cardName.value = card.name - self.cardLastDigits.value = card.last4 + self.cardName.value = card.cardInfo.cardholderName ?? "" + self.cardLastDigits.value = card.cardInfo.lastFour ?? "" - self.cardToken.value = card.cardToken + self.cardToken.value = card.token if let newUser = self.navigationController?.fulfillmentNav().bidDetails.newUser { - newUser.name.value = (newUser.name.value.isNilOrEmpty) ? card.name : newUser.name.value + newUser.name.value = (newUser.name.value.isNilOrEmpty) ? card.cardInfo.cardholderName : newUser.name.value } } @@ -116,7 +125,6 @@ class SwipeCreditCardViewController: UIViewController, RegistrationSubController func setInProgress(_ show: Bool) { illustrationImageView.alpha = show ? 0.1 : 1 - processingLabel.isHidden = !show spinner.isHidden = !show } diff --git a/Kiosk/Storyboards/Fulfillment.storyboard b/Kiosk/Storyboards/Fulfillment.storyboard index 09862832..688740ea 100644 --- a/Kiosk/Storyboards/Fulfillment.storyboard +++ b/Kiosk/Storyboards/Fulfillment.storyboard @@ -1,11 +1,11 @@ - + - + @@ -90,7 +90,6 @@ - @@ -114,7 +113,7 @@ - + @@ -122,7 +121,7 @@ - + @@ -193,7 +192,7 @@ - + @@ -212,7 +211,7 @@ - + @@ -220,7 +219,7 @@ - + @@ -272,7 +271,7 @@ - @@ -453,7 +446,6 @@ - @@ -477,7 +469,7 @@ - + @@ -495,7 +487,7 @@ - + - + - + @@ -661,7 +653,7 @@ - + - + @@ -735,7 +727,7 @@ - + @@ -798,7 +790,7 @@ - + @@ -807,7 +799,7 @@ - + @@ -847,7 +839,7 @@ - + @@ -856,7 +848,7 @@ - + - + @@ -914,7 +906,7 @@