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

Conversation

porter-stripe
Copy link
Collaborator

@porter-stripe porter-stripe commented Jan 22, 2025

Summary

  • Instead of just hiding disallowed card brands in the CBC dropdown, we should instead show them as disabled and prevent them from being selected. When the user attempts to scroll to a disallowed brand it is "rolled back" to the previous selection.

Update card flow

Simulator.Screen.Recording.-.iOS.18.-.2025-01-22.at.09.45.28.mp4

New card flow

Simulator.Screen.Recording.-.iOS.18.-.2025-01-23.at.09.25.00.mp4

Motivation

https://jira.corp.stripe.com/browse/MOBILESDK-3035

Testing

  • New unit test
  • Manual

Changelog

N/A (private beta for now)

@stripe stripe deleted a comment from github-actions bot Jan 23, 2025
Copy link

github-actions bot commented Jan 23, 2025

⚠️ Missing Translations
The following strings have been uploaded to Lokalise but are not yet translated.

(not accepted)

If it's okay for these strings to be unlocalized in master (e.g. this is for an unshipped feature), add the label ship without translations to acknowledge that there are missing translations. Otherwise, wait until translations are available in Lokalise and re-run this job.

New strings are localized on a weekly basis and are downloaded as part of the release process. For more details on how to localize a string, you can refer to this link.

@porter-stripe
Copy link
Collaborator Author

porter-stripe commented Jan 23, 2025

Added the "ship without translations" b/c:

  1. This feature is in private beta
  2. None of the merchants using this feature use CBC

By the time we GA this feature translations will have come in, we won't GA until they do.

@stripe stripe deleted a comment from github-actions bot Jan 27, 2025
@stripe stripe deleted a comment from github-actions bot Jan 27, 2025
@porter-stripe porter-stripe marked this pull request as ready for review January 28, 2025 16:01
@porter-stripe porter-stripe requested review from a team as code owners January 28, 2025 16:01
case .USBankAccount, .SEPADebit:
return false
default:
fatalError("Updating payment method has not been implemented for \(paymentMethod.type)")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of fatal Error, we should be using stpAssert to avoid being noisy for integrators, but..taking a step back, is it really needed to have a fatal error/assert? Seems the function should just be something like:

var isUpdatableCardBrandCard: Bool {
    guard paymentMethod.type == .card {
        return false
    }
    let availableBrands ...
    etc...
    return...
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I like this, good idea.

Comment on lines 103 to 104
let itemsWithDisabled = items + [disabledItem]

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like we should assert the number of items before directly accessing specific indexes.

@@ -265,6 +271,13 @@ extension DropdownFieldElement {
}

public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let item = items[row]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like we should add to the top of the fn:

        guard row < items.count else {
            stpAssertionFailure("...")
            return
        }

@@ -26,17 +27,21 @@ extension STPCardBrand {
return NSAttributedString(attachment: brandImageAttachment)
}

func cardBrandItem(theme: ElementsAppearance = .default, maxWidth: CGFloat? = nil) -> DropdownFieldElement.DropdownItem {
func cardBrandItem(theme: ElementsAppearance = .default, isAllowed: Bool, maxWidth: CGFloat? = nil) -> DropdownFieldElement.DropdownItem {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be good to keep a consistent nomenclature of disallowed instead of introducing isAllowed

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah agree!

Comment on lines 256 to 271
let validBrandSelections = cardBrandDropDown.items.filter { !$0.isPlaceholder && !$0.isDisabled }

// 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 brandToSelect = preferredNetworks.first(where: { fetchedCardBrands.contains($0) && !disallowedCardBrands.contains($0) }),
let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == STPCardBrandUtilities.apiValue(from: brandToSelect) }) {
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.
} else if validBrandSelections.count == 1,
!disallowedCardBrands.isEmpty,
let firstItem = validBrandSelections.first,
let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == firstItem.rawData }) {
// 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.
cardBrandDropDown.select(index: 1, shouldAutoAdvance: false)
cardBrandDropDown.select(index: indexToSelect, shouldAutoAdvance: false)
Copy link
Collaborator

@wooj-stripe wooj-stripe Jan 29, 2025

Choose a reason for hiding this comment

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

I'm having a tough time following this logic, so i tried to refactor it a bit.... Let me know what you think about breaking up this logic in the following way:

   ....
               cardBrandDropDown.update(items: DropdownFieldElement.items(...)
   ....
               // 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)
               }

Then we take the logic out of those ifstatements into two separate functions:

        // 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
        }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah I like this better!

@porter-stripe porter-stripe enabled auto-merge (squash) January 29, 2025 15:19
@porter-stripe porter-stripe merged commit 35423e6 into master Jan 29, 2025
7 checks passed
@porter-stripe porter-stripe deleted the porter/cbf-cbc-ga branch January 29, 2025 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants