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

Show blocked brands in CBC dropdown, but block selection #4489

Merged
merged 17 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -163,8 +163,6 @@
6141C5072C0A47A700E81735 /* RightAccessoryButtonTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141C5062C0A47A700E81735 /* RightAccessoryButtonTest.swift */; };
614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */; };
6151DDC02B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */; };
615AADAF2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615AADAE2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift */; };
615AADB12CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */; };
615C2C502CBDBA61003F0173 /* EmbeddedFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C2C4F2CBDBA61003F0173 /* EmbeddedFormViewController.swift */; };
6180A5C12C8222A9009D1536 /* EmbeddedPaymentMethodsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6180A5C02C8222A9009D1536 /* EmbeddedPaymentMethodsView.swift */; };
6180A5C72C824377009D1536 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6180A5C62C824377009D1536 /* RadioButton.swift */; };
Expand Down Expand Up @@ -590,8 +588,6 @@
614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentMethodsViewTests.swift; sourceTree = "<group>"; };
6141C5062C0A47A700E81735 /* RightAccessoryButtonTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightAccessoryButtonTest.swift; sourceTree = "<group>"; };
6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePaymentMethodViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
615AADAE2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPCardValidator+BrandFiltering.swift"; sourceTree = "<group>"; };
615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPCardValidator+BrandFilteringTest.swift"; sourceTree = "<group>"; };
615C2C4F2CBDBA61003F0173 /* EmbeddedFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedFormViewController.swift; sourceTree = "<group>"; };
617C44F9338DE2E93E318291 /* PayWithLinkWebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayWithLinkWebController.swift; sourceTree = "<group>"; };
6180A5C02C8222A9009D1536 /* EmbeddedPaymentMethodsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentMethodsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1071,7 +1067,6 @@
2C59FD8C17CA1D740BCAFA4D /* STPPaymentIntentShippingDetailsParams+PaymentSheet.swift */,
61D842882CADE4B9009D2D51 /* PaymentElementConfiguration.swift */,
61C87E1A2CB818ED001B7DA9 /* CardBrandFilter.swift */,
615AADAE2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift */,
);
path = PaymentSheet;
sourceTree = "<group>";
Expand Down Expand Up @@ -1719,7 +1714,6 @@
E09C073021CE89593466548C /* STPCardBrandChoiceTest.swift */,
E198795E4D8677B6ECFCEEDC /* STPElementsSessionTest.swift */,
A39F7EBA2E9E3CE55E7AADC9 /* STPFixtures+PaymentSheet.swift */,
615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */,
AC0DBA7D63BAD0182695E436 /* Stubbed */,
D00C7F5905759525C9BF8BD4 /* TextFieldElement+CardTest.swift */,
6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */,
Expand Down Expand Up @@ -2036,7 +2030,6 @@
041E3F2DFDFD8FA7D3353CDB /* PaymentSheetSnapshotTests.swift in Sources */,
1330B53140DE10F641A82099 /* PaymentSheetViewControllerSnapshotTests.swift in Sources */,
614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */,
615AADB12CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift in Sources */,
619AF08A2BF56FC000D1C981 /* VerticalSavedPaymentMethodsViewControllerTests.swift in Sources */,
47AD56A9889DF5EFBBA9CEFB /* PollingViewTests.swift in Sources */,
61ED657C2D41AE1A00DD5E92 /* PaymentSheet+PaymentMethodAvailabilityTest.swift in Sources */,
Expand Down Expand Up @@ -2242,7 +2235,6 @@
49803444CD948F1ED28FF021 /* PaymentSheetFormFactory+FormSpec.swift in Sources */,
B61E2C202C5C44FE0045B5CF /* PaymentSheetAnalyticsHelper.swift in Sources */,
C5E3750BBCA700CF364F7578 /* PaymentSheetFormFactory+OXXO.swift in Sources */,
615AADAF2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift in Sources */,
31AD3BE72B0C2D080080C800 /* UIApplication+StripePaymentSheet.swift in Sources */,
9806232CE48077E35B04FF98 /* PaymentSheetFormFactory+UPI.swift in Sources */,
401128A8DDC7B6E3CBB4381E /* PaymentSheetFormFactory.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ extension CustomerSavedPaymentMethodsCollectionViewController: PaymentOptionCell
hostedSurface: .customerSheet,
cardBrandFilter: savedPaymentMethodsConfiguration.cardBrandFilter,
canRemove: configuration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod),
canUpdateCardBrand: paymentMethod.isCoBrandedCard && cbcEligible)
isCBCEligible: paymentMethod.isCoBrandedCard && cbcEligible)
let editVc = UpdatePaymentMethodViewController(
removeSavedPaymentMethodMessage: savedPaymentMethodsConfiguration.removeSavedPaymentMethodMessage,
isTestMode: configuration.isTestMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,15 @@ final class CardSectionElement: ContainerElement {
// Clear any previously fetched card brands from the dropdown
if !self.cardBrands.isEmpty {
self.cardBrands = Set<STPCardBrand>()
cardBrandDropDown?.update(items: DropdownFieldElement.items(from: self.cardBrands, theme: self.theme))
cardBrandDropDown?.update(items: DropdownFieldElement.items(from: self.cardBrands, disallowedCardBrands: Set<STPCardBrand>(), theme: self.theme))
self.panElement.setText(self.panElement.text) // Hack to get the accessory view to update
}
return
}

var fetchedCardBrands = Set<STPCardBrand>()
let hadBrands = !cardBrands.isEmpty
STPCardValidator.possibleBrands(forNumber: panElement.text, with: cardBrandFilter) { [weak self] result in
STPCardValidator.possibleBrands(forNumber: panElement.text) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let brands):
Expand All @@ -245,25 +245,51 @@ final class CardSectionElement: ContainerElement {

if self.cardBrands != fetchedCardBrands {
self.cardBrands = fetchedCardBrands
cardBrandDropDown.update(items: DropdownFieldElement.items(from: fetchedCardBrands, theme: self.theme))

// If we didn't previously have brands but now have them select based on merchant preference
// Select the first brand in the fetched brands that appears earliest in the merchants preferred networks
if !hadBrands,
let preferredNetworks = self.preferredNetworks,
let brandToSelect = preferredNetworks.first(where: { fetchedCardBrands.contains($0) }),
let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == STPCardBrandUtilities.apiValue(from: brandToSelect) }) {
let disallowedCardBrands = fetchedCardBrands.filter { !self.cardBrandFilter.isAccepted(cardBrand: $0) }

cardBrandDropDown.update(items: DropdownFieldElement.items(
from: fetchedCardBrands,
disallowedCardBrands: disallowedCardBrands,
theme: self.theme
))

// Prioritize merchant preference if we did not have brands prior to calling .possibleBrands, otherwise use default logic
if !hadBrands, let indexToSelect = hasPreferredBrandIndex(fetchedCardBrands: fetchedCardBrands, disallowedCardBrands: disallowedCardBrands, cardBrandDropDown: cardBrandDropDown) {
cardBrandDropDown.select(index: indexToSelect, shouldAutoAdvance: false)
} else if let indexToSelect = useDefaultSelectionLogic(disallowedCardBrands: disallowedCardBrands, cardBrandDropDown: cardBrandDropDown) {
cardBrandDropDown.select(index: indexToSelect, shouldAutoAdvance: false)
} else if cardBrands.count == 1 && self.cardBrandFilter != .default {
// If we only fetched one card brand auto select it, 1 index due to 0 index being the placeholder.
// This case typically only occurs when card brand filtering is used with CBC and one of the fetched brands is filtered out.
cardBrandDropDown.select(index: 1, shouldAutoAdvance: false)
}

self.panElement.setText(self.panElement.text) // Hack to get the accessory view to update
}
}
}

// Select the first brand in the fetched brands that appears earliest in the merchants preferred networks
func hasPreferredBrandIndex(fetchedCardBrands: Set<STPCardBrand>, disallowedCardBrands: Set<STPCardBrand>, cardBrandDropDown: DropdownFieldElement) -> Int? {
guard let preferredNetworks = self.preferredNetworks,
let brandToSelect = preferredNetworks.first(where: { fetchedCardBrands.contains($0) && !disallowedCardBrands.contains($0) }),
let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == STPCardBrandUtilities.apiValue(from: brandToSelect) }) else {
return nil
}

return indexToSelect

}

// If we only fetched one card brand that is not disallowed, auto select it.
// This case typically only occurs when card brand filtering is used with CBC and one of the fetched brands is filtered out.
func useDefaultSelectionLogic(disallowedCardBrands: Set<STPCardBrand>, cardBrandDropDown: DropdownFieldElement) -> Int? {
let validBrandSelections = cardBrandDropDown.items.filter { !$0.isPlaceholder && !$0.isDisabled }
guard validBrandSelections.count == 1,
!disallowedCardBrands.isEmpty,
let firstItem = validBrandSelections.first,
let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == firstItem.rawData }) else {
return nil
}

return indexToSelect
}
}

// MARK: - Helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ extension EmbeddedPaymentElement: EmbeddedPaymentMethodsViewDelegate {
hostedSurface: .paymentSheet,
cardBrandFilter: configuration.cardBrandFilter,
canRemove: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(),
canUpdateCardBrand: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible,
isCBCEligible: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
isDefault: paymentMethod == elementsSession.customer?.getDefaultPaymentMethod()
)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ extension SavedPaymentOptionsViewController: PaymentOptionCellDelegate {
hostedSurface: .paymentSheet,
cardBrandFilter: paymentSheetConfiguration.cardBrandFilter,
canRemove: configuration.allowsRemovalOfPaymentMethods && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod),
canUpdateCardBrand: paymentMethod.isCoBrandedCard && cbcEligible,
isCBCEligible: paymentMethod.isCoBrandedCard && cbcEligible,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
isDefault: paymentMethod == elementsSession.customer?.getDefaultPaymentMethod()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ extension VerticalSavedPaymentMethodsViewController: SavedPaymentMethodRowButton
hostedSurface: .paymentSheet,
cardBrandFilter: configuration.cardBrandFilter,
canRemove: canRemovePaymentMethods,
canUpdateCardBrand: paymentMethod.isCoBrandedCard && isCBCEligible,
isCBCEligible: paymentMethod.isCoBrandedCard && isCBCEligible,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
isDefault: paymentMethod == elementsSession.customer?.getDefaultPaymentMethod()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import UIKit
extension SavedPaymentMethodFormFactory {
func makeCard() -> Element {
let cardBrandDropDown: DropdownFieldElement? = {
guard viewModel.canUpdateCardBrand else { return nil }
let cardBrands = viewModel.paymentMethod.card?.networks?.available.map({ STPCard.brand(from: $0) }).filter { viewModel.cardBrandFilter.isAccepted(cardBrand: $0) } ?? []
guard viewModel.isCBCEligible else { return nil }
let cardBrands = viewModel.paymentMethod.card?.networks?.available.map({ STPCard.brand(from: $0) }) ?? []
let disallowedCardBrands = cardBrands.filter{ !viewModel.cardBrandFilter.isAccepted(cardBrand: $0) }

let cardBrandDropDown = DropdownFieldElement.makeCardBrandDropdown(cardBrands: Set<STPCardBrand>(cardBrands),
disallowedCardBrands: Set<STPCardBrand>(disallowedCardBrands),
theme: viewModel.appearance.asElementsTheme,
includePlaceholder: false) { [weak self] in
guard let self = self else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo
hostedSurface: .paymentSheet,
cardBrandFilter: configuration.cardBrandFilter,
canRemove: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(),
canUpdateCardBrand: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible,
isCBCEligible: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible,
allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM,
isDefault: paymentMethod == elementsSession.customer?.getDefaultPaymentMethod()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class UpdatePaymentMethodViewModel {
let hostedSurface: HostedSurface
let cardBrandFilter: CardBrandFilter
let canRemove: Bool
let canUpdateCardBrand: Bool
let isCBCEligible: Bool
let allowsSetAsDefaultPM: Bool
let isDefault: Bool

Expand All @@ -32,6 +32,14 @@ class UpdatePaymentMethodViewModel {
var hasUpdates: Bool {
return hasChangedCardBrand || hasChangedDefaultPaymentMethodCheckbox
}
var canUpdateCardBrand: Bool {
guard paymentMethod.type == .card else {
return false
}
let availableBrands = paymentMethod.card?.networks?.available.map {$0.toCardBrand }.compactMap{ $0 }
let filteredCardBrands = availableBrands?.filter {cardBrandFilter.isAccepted(cardBrand: $0)} ?? []
return isCBCEligible && filteredCardBrands.count > 1
}

lazy var header: String = {
switch paymentMethod.type {
Expand Down Expand Up @@ -59,7 +67,7 @@ class UpdatePaymentMethodViewModel {
}
}()

init(paymentMethod: STPPaymentMethod, appearance: PaymentSheet.Appearance, hostedSurface: HostedSurface, cardBrandFilter: CardBrandFilter = .default, canRemove: Bool, canUpdateCardBrand: Bool, allowsSetAsDefaultPM: Bool = false, isDefault: Bool = false) {
init(paymentMethod: STPPaymentMethod, appearance: PaymentSheet.Appearance, hostedSurface: HostedSurface, cardBrandFilter: CardBrandFilter = .default, canRemove: Bool, isCBCEligible: Bool, allowsSetAsDefaultPM: Bool = false, isDefault: Bool = false) {
guard PaymentSheet.supportedSavedPaymentMethods.contains(paymentMethod.type) else {
fatalError("Unsupported payment type \(paymentMethod.type) in UpdatePaymentMethodViewModel")
}
Expand All @@ -68,7 +76,7 @@ class UpdatePaymentMethodViewModel {
self.hostedSurface = hostedSurface
self.cardBrandFilter = cardBrandFilter
self.canRemove = canRemove
self.canUpdateCardBrand = canUpdateCardBrand
self.isCBCEligible = isCBCEligible
self.allowsSetAsDefaultPM = allowsSetAsDefaultPM
self.isDefault = isDefault
}
Expand Down
Loading
Loading