Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Co-badged Cards on Drop-in #1050

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/primer-io/primer-sdk-3ds-ios",
"state": {
"branch": null,
"revision": "ac3be93adcc4d054eef7baf0f4d364cd3525dbf7",
"version": "2.3.2"
"revision": "7d2c9ac8825a4459034a1416012cae61761543fd",
"version": "2.4.1"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ class MerchantHeadlessCheckoutRawDataViewController: UIViewController {
extension MerchantHeadlessCheckoutRawDataViewController: UITextFieldDelegate {

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

let text = textField.text

var newText: String = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public class PrimerCardNetwork: NSObject {
guard let network = network else { return nil }
self.init(network: network)
}

override public var description: String {
return "PrimerCardNetwork(displayName: \(displayName), network: \(network), allowed: \(allowed))"
}
}

@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ class CardFormPaymentMethodTokenizationViewModel: PaymentMethodTokenizationViewM
return manager
}()

// Used for Co-Badged Cards feature
private let cardPaymentMethodName = "PAYMENT_CARD"
private lazy var rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager? = {
// If the manager is not resolved (nil) Co-Badged cards feature will just not work and the card form should be working as before
let manager = try? PrimerHeadlessUniversalCheckout.RawDataManager(paymentMethodType: cardPaymentMethodName, delegate: self)
return manager
}()

private var rawCardData = PrimerCardData(cardNumber: "",
expiryDate: "",
cvv: "",
cardholderName: "")
fileprivate var currentlyAvailableCardNetworks: [PrimerCardNetwork]?

private let theme: PrimerThemeProtocol = DependencyContainer.resolve()

var userInputCompletion: (() -> Void)?
Expand Down Expand Up @@ -81,9 +95,17 @@ class CardFormPaymentMethodTokenizationViewModel: PaymentMethodTokenizationViewM
return textField
}()

var cardNetwork: CardNetwork? {
var defaultCardNetwork: CardNetwork? {
didSet {
cvvField.cardNetwork = cardNetwork ?? .unknown
cvvField.cardNetwork = defaultCardNetwork ?? .unknown
}
}

var alternativelySelectedCardNetwork: CardNetwork? {
didSet {
if let alternativelySelectedCardNetwork {
cvvField.cardNetwork = alternativelySelectedCardNetwork
}
}
}

Expand All @@ -106,7 +128,24 @@ class CardFormPaymentMethodTokenizationViewModel: PaymentMethodTokenizationViewM
}()

private lazy var cardNumberContainerView: PrimerCustomFieldView = {
PrimerCardNumberField.cardNumberContainerViewWithFieldView(cardNumberField)
let containerView = PrimerCardNumberField.cardNumberContainerViewWithFieldView(cardNumberField)
containerView.onCardNetworkSelected = { [weak self] cardNetwork in
guard let self = self else { return }
self.alternativelySelectedCardNetwork = cardNetwork.network
self.rawCardData.cardNetwork = cardNetwork.network
self.rawDataManager?.rawData = self.rawCardData // TODO: (BNI) This does not work for unknown reason
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ TODOs should be resolved ((BNI) This does not work for u...). (todo)


// Select payment method based on the detected card network
let clientSessionActionsModule: ClientSessionActionsProtocol = ClientSessionActionsModule()
firstly {
clientSessionActionsModule.selectPaymentMethodIfNeeded(self.config.type, cardNetwork: cardNetwork.network.rawValue)
}
.done {
self.updateButtonUI()
}
.catch { _ in }
}
return containerView
}()

// MARK: - Cardholder name field
Expand Down Expand Up @@ -660,7 +699,7 @@ class CardFormPaymentMethodTokenizationViewModel: PaymentMethodTokenizationViewM
func configurePayButton(cardNetwork: CardNetwork?) {
var amount: Int = AppState.current.amount ?? 0

if let surcharge = cardNetwork?.surcharge {
if let surcharge = alternativelySelectedCardNetwork?.surcharge ?? cardNetwork?.surcharge {
amount += surcharge
}

Expand Down Expand Up @@ -710,7 +749,7 @@ extension CardFormPaymentMethodTokenizationViewModel {

private func dispatchActions() -> Promise<Void> {
return Promise { seal in
var network = self.cardNetwork?.rawValue.uppercased()
var network = self.alternativelySelectedCardNetwork?.rawValue.uppercased() ?? self.defaultCardNetwork?.rawValue.uppercased()
if network == nil || network == "UNKNOWN" {
network = "OTHER"
}
Expand Down Expand Up @@ -910,29 +949,48 @@ extension CardFormPaymentMethodTokenizationViewModel: PrimerTextFieldViewDelegat
}

func primerTextFieldView(_ primerTextFieldView: PrimerTextFieldView, didDetectCardNetwork cardNetwork: CardNetwork?) {
self.cardNetwork = cardNetwork
self.defaultCardNetwork = cardNetwork

if let text = primerTextFieldView.textField.internalText {
rawCardData.cardNumber = text.replacingOccurrences(of: " ", with: "")
rawDataManager?.rawData = rawCardData
}

DispatchQueue.main.async {
self.handleCardNetworkDetection(cardNetwork)
}
}

private func handleCardNetworkDetection(_ cardNetwork: CardNetwork?) {
guard alternativelySelectedCardNetwork == nil else { return }

self.rawCardData.cardNetwork = cardNetwork
self.rawDataManager?.rawData = self.rawCardData

var network = self.cardNetwork?.rawValue.uppercased()
var network = cardNetwork?.rawValue.uppercased()
let clientSessionActionsModule: ClientSessionActionsProtocol = ClientSessionActionsModule()

if let cardNetwork = cardNetwork,
cardNetwork != .unknown,
cardNumberContainerView.rightImage2 != cardNetwork.icon {
if let cardNetwork = cardNetwork, cardNetwork != .unknown, cardNumberContainerView.rightImageView.image != cardNetwork.icon {
// Set the network value to "OTHER" if it's nil or unknown
if network == nil || network == "UNKNOWN" {
network = "OTHER"
}

cardNumberContainerView.rightImage2 = cardNetwork.icon
// Update the UI with the detected card network icon
cardNumberContainerView.rightImage = cardNetwork.icon

// Select payment method based on the detected card network
firstly {
clientSessionActionsModule.selectPaymentMethodIfNeeded(self.config.type, cardNetwork: network)
}
.done {
self.updateButtonUI()
}
.catch { _ in }
} else if cardNumberContainerView.rightImage2 != nil && (cardNetwork?.icon == nil || cardNetwork == .unknown) {
cardNumberContainerView.rightImage2 = nil

} else if cardNumberContainerView.rightImage != nil && (cardNetwork?.icon == nil || cardNetwork == .unknown) {
// Unselect payment method and remove the card network icon if unknown or nil
cardNumberContainerView.rightImage = nil

firstly {
clientSessionActionsModule.unselectPaymentMethodIfNeeded()
Expand Down Expand Up @@ -1039,6 +1097,71 @@ extension CardFormPaymentMethodTokenizationViewModel: UITextFieldDelegate {
}
}

// MARK: - PrimerHeadlessUniversalCheckoutRawDataManagerDelegate
extension CardFormPaymentMethodTokenizationViewModel: PrimerHeadlessUniversalCheckoutRawDataManagerDelegate {

func primerRawDataManager(_ rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager,
dataIsValid isValid: Bool,
errors: [Swift.Error]?) {
let errorsDescription = errors?.map { $0.localizedDescription }.joined(separator: ", ")
logger.debug(message: "dataIsValid: \(isValid), errors: \(errorsDescription ?? "none")")
}

func primerRawDataManager(_ rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager,
metadataDidChange metadata: [String: Any]?) {
logger.debug(message: "metadataDidChange: \(metadata ?? [:])")
}

func primerRawDataManager(_ rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager,
willFetchMetadataForState cardState: PrimerValidationState) {
guard let state = cardState as? PrimerCardNumberEntryState else {
logger.error(message: "Received non-card metadata. Ignoring ...")
return
}
logger.debug(message: "willFetchCardMetadataForState: \(state.cardNumber)")
}

func primerRawDataManager(_ rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager,
didReceiveMetadata metadata: PrimerPaymentMethodMetadata,
forState cardState: PrimerValidationState) {

guard let metadata = metadata as? PrimerCardNumberEntryMetadata,
let cardState = cardState as? PrimerCardNumberEntryState else {
logger.error(message: "Received non-card metadata. Ignoring ...")
return
}

let metadataDescription = metadata.selectableCardNetworks?.items.map { $0.displayName }.joined(separator: ", ") ?? "n/a"
logger.debug(message: "didReceiveCardMetadata: (selectable ->) \(metadataDescription), cardState: \(cardState.cardNumber)")

if metadata.source == .remote, let networks = metadata.selectableCardNetworks?.items, !networks.isEmpty {
currentlyAvailableCardNetworks = metadata.selectableCardNetworks?.items
} else if let preferredDetectedNetwork = metadata.detectedCardNetworks.preferred {
currentlyAvailableCardNetworks = [preferredDetectedNetwork]
} else if let cardNetwork = metadata.detectedCardNetworks.items.first {
currentlyAvailableCardNetworks = [cardNetwork]
} else {
currentlyAvailableCardNetworks = []
}

currentlyAvailableCardNetworks = currentlyAvailableCardNetworks?.filter { $0.displayName != "Unknown" }
cardNumberContainerView.cardNetworks = currentlyAvailableCardNetworks ?? []

if currentlyAvailableCardNetworks?.count ?? 0 < 2 {
self.alternativelySelectedCardNetwork = nil
DispatchQueue.main.async {
self.cardNumberContainerView.resetCardNetworkSelection()
self.handleCardNetworkDetection(self.currentlyAvailableCardNetworks?.first?.network)
}
}
}

private func image(from model: PrimerCardNetwork) -> UIImage? {
let asset = PrimerHeadlessUniversalCheckout.AssetsManager.getCardNetworkAsset(for: model.network)
return asset?.cardImage
}
}

private extension String {
func lowercasedAndFolded() -> String {
self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ protocol InternalCardComponentsManagerProtocol {
var expiryDateField: PrimerExpiryDateFieldView { get }
var cvvField: PrimerCVVFieldView { get }
var cardholderField: PrimerCardholderNameFieldView? { get }
var selectedCardNetwork: CardNetwork? { get }
var delegate: InternalCardComponentsManagerDelegate { get }
var customerId: String? { get }
var merchantIdentifier: String? { get }
Expand All @@ -61,6 +62,7 @@ internal class InternalCardComponentsManager: NSObject, InternalCardComponentsMa
var expiryDateField: PrimerExpiryDateFieldView
var cvvField: PrimerCVVFieldView
var cardholderField: PrimerCardholderNameFieldView?
var selectedCardNetwork: CardNetwork? // Network selected by the customer in Co-Badged Cards feature
var billingAddressFieldViews: [PrimerTextFieldView]?
var isRequiringCVVInput: Bool
var paymentMethodType: String
Expand Down Expand Up @@ -236,7 +238,7 @@ and 4 characters for expiry year separated by '/'.
diagnosticsId: UUID().uuidString)
errors.append(err)

} else if !cvvField.cvv.isValidCVV(cardNetwork: CardNetwork(cardNumber: cardnumberField.cardnumber)) {
} else if !cvvField.cvv.isValidCVV(cardNetwork: selectedCardNetwork ?? CardNetwork(cardNumber: cardnumberField.cardnumber)) {
let err = PrimerValidationError.invalidCvv(
message: "CVV is not valid.",
userInfo: .errorUserInfoDictionary(),
Expand Down Expand Up @@ -292,7 +294,8 @@ and 4 characters for expiry year separated by '/'.
cvv: self.cvvField.cvv,
expirationMonth: expiryMonth,
expirationYear: cardExpirationYear,
cardholderName: self.cardholderField?.cardholderName)
cardholderName: self.cardholderField?.cardholderName,
preferredNetwork: self.selectedCardNetwork?.rawValue)
return cardPaymentInstrument

} else if let configId = AppState.current.apiConfiguration?.getConfigId(for: self.primerPaymentMethodType.rawValue),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ class PrimerCardFormViewController: PrimerFormViewController {
private let theme: PrimerThemeProtocol = DependencyContainer.resolve()
private let formPaymentMethodTokenizationViewModel: CardFormPaymentMethodTokenizationViewModel

init(navigationBarLogo: UIImage? = nil, viewModel: CardFormPaymentMethodTokenizationViewModel) {
self.formPaymentMethodTokenizationViewModel = viewModel
init(navigationBarLogo: UIImage? = nil,
viewModel: CardFormPaymentMethodTokenizationViewModel) {
formPaymentMethodTokenizationViewModel = viewModel
super.init(nibName: nil, bundle: nil)
self.titleImage = navigationBarLogo
if self.titleImage == nil {
titleImage = navigationBarLogo
if titleImage == nil {
title = Strings.PrimerCardFormView.title
}
}
Expand Down
Loading
Loading