-
Notifications
You must be signed in to change notification settings - Fork 995
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
Read the default_payment_method field from elements session and use that as the default PM #4313
Conversation
🚨 New dead code detected in this PR: String+Localized.swift:352 warning: Property 'remove_payment_method' is unused
SavedPaymentMethodCollectionView.swift:113 warning: Property 'cbcEligible' is assigned, but never used
SavedPaymentMethodRowButton.swift:16 warning: Function 'didSelectRemoveButton(_:with:)' is unused
SavedPaymentMethodRowButton.swift:70 warning: Property 'canRemove' is unused
SavedPaymentMethodRowButton.swift:117 warning: Function 'handleRemoveButtonTapped()' is unused
VerticalSavedPaymentMethodsViewController.swift:99 warning: Property 'hasCoBrandedCards' is unused
VerticalSavedPaymentMethodsViewController.swift:312 warning: Function 'didSelectRemoveButton(_:with:)' is unused
PaymentSheetUIKitAdditions.swift:164 warning: Function 'makeRowButtonContentStackView(arrangedSubviews:)' is unused Please remove the dead code before merging. If this is intentional, you can bypass this check by adding the label ℹ️ If this comment appears to be left in error, double check that the flagged code is actually used and/or make sure your branch is up-to-date with |
…s into joyceqin-MOBILESDK-2799
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of duplicated logic that is very similar, we should try to consolidate that into one place as much as possible.
@@ -53,6 +53,9 @@ struct CustomerSheetTestPlayground: View { | |||
SettingPickerView(setting: $playgroundController.settings.paymentMethodRemove) | |||
SettingPickerView(setting: $playgroundController.settings.paymentMethodRemoveLast) | |||
SettingPickerView(setting: $playgroundController.settings.paymentMethodAllowRedisplayFilters) | |||
if playgroundController.settings.alternateUpdatePaymentMethodNavigation == .on { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When can we remove alternateUpdatePaymentMethodNavigation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose if we're trying to release this is the default flow, it should be removed now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do anticipate possibly having to update a lot of tests though, so might be changing a lot of files
// to test default payment methods reading from back end, hard-code a valid default payment method | ||
// later, when API calls to get and update default payment method are available, that will no longer be needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn't make much sense to me, I don't see any code changes near this comment indicating we are hard coding a default PM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I was trying to say is that the only way I knew to test it was to add a payment method, get its stripe ID, and then hardcode that here so that the elements session will set that as its default. And then after adding other cards, even after paying with them, when the payment sheet is reloaded, it will have the hardcoded default selected, which shows that it's reading from the elements session instead of the local default.
Right now, there's a duping process where when you read from "default_payment_method", it returns the locally saved default that you pass into the API call— it's not actually retrieving a default from the back end. When the API endpoints are ready, then we should be able to actually get the value stored in the back end, but until then, the only way I could think to test that it's reading from the elementsSession value instead of the local default was to hard-code a default (e.g. let defaultPaymentMethod = "pm_1QSOUILu5o3P18ZpN4PY49SE")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm, I see what you mean, but don't think we need a comment for that. I don't think anyone else will need this comment as it shouldn't be relevant once this feature is implemented. If you want to leave it let's add a TODO to remove it for yourself.
@@ -107,7 +107,13 @@ extension CustomerSessionAdapter { | |||
return stripePaymentMethodId | |||
} | |||
|
|||
func fetchSelectedPaymentOption(for customerId: String) -> CustomerPaymentOption? { | |||
func fetchSelectedPaymentOption(for customerId: String, elementsSession: STPElementsSession? = nil) -> CustomerPaymentOption? { | |||
if configuration.allowsSetAsDefaultPM, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit; I think a guard here is more appropriate as it enforces that you return a value:
guard configuration.allowsSetAsDefaultPM,
let elementsSession = elementsSession,
let customer = elementsSession.customer,
let defaultPaymentMethod = customer.defaultPaymentMethod else {
return CustomerPaymentOption.defaultPaymentMethod(for: customerId)
}
return CustomerPaymentOption.stripeId(defaultPaymentMethod)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it seems unnecessary to pass in the entire STPElementsSession
here, we only really need the customer.
@@ -86,6 +86,11 @@ extension CustomerSheet { | |||
/// If false (default), only card brand choice eligible cards can be edited and users can remove payment methods from the list screen. | |||
@_spi(AlternateUpdatePaymentMethodNavigation) public var alternateUpdatePaymentMethodNavigation = false | |||
|
|||
/// This is an experimental feature that may be removed at any time. | |||
/// If true, users can set a payment method as default. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add that this also influences where we read the default payment method from, not just allow setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about "users can set a payment method as default and sync their default payment method across web and mobile"
|
||
var selectedPaymentOption: CustomerPaymentOption? | ||
|
||
// read from back end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment could be better!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it to "get default payment method from elements session"— is that better?
/// This is an experimental feature that may be removed at any time. | ||
/// If true, users can set a payment method as default. | ||
/// If false (default), users cannot set default payment methods. | ||
@_spi(AllowsSetAsDefaultPM) public var allowsSetAsDefaultPM = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment from about in the CustomerSheetConfiguration. Also same in PaymentSheetConfiguration.
// read from back end | ||
if configuration.allowsSetAsDefaultPM, | ||
let customer = loadResult.elementsSession.customer { | ||
let defaultPaymentMethod = customer.paymentMethods.filter { | ||
$0.stripeId == customer.defaultPaymentMethod | ||
}.first | ||
if let defaultPaymentMethod = defaultPaymentMethod { | ||
return .saved(paymentMethod: defaultPaymentMethod) | ||
} | ||
|
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat similar comment to above, about the "read from back end" comment and the nested if's
Maybe:
if configuration.allowsSetAsDefaultPM,
let customer = loadResult.elementsSession.customer,
let defaultPaymentMethodId = customer.defaultPaymentMethod,
let defaultPaymentMethod = customer.paymentMethods.first(where: { $0.stripeId == defaultPaymentMethodId }) {
return .saved(paymentMethod: defaultPaymentMethod)
}
// read from back end | ||
if configuration.allowsSetAsDefaultPM, | ||
let customer = elementsSession.customer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar here w/ the comment and nested if's and multiple branches leading to the same CustomerPaymentOption.defaultPaymentMethod(for: customerID)
. I think there's a cleaner way to write this.
@@ -438,9 +444,22 @@ class SavedPaymentOptionsViewController: UIViewController { | |||
|
|||
/// Creates the list of viewmodels to display in the "saved payment methods" carousel e.g. `["+ Add", "Apple Pay", "Link", "Visa 4242"]` | |||
/// - Returns defaultSelectedIndex: The index of the view model that is the default e.g. in the above list, if "Visa 4242" is the default, the index is 3. | |||
static func makeViewModels(savedPaymentMethods: [STPPaymentMethod], customerID: String?, showApplePay: Bool, showLink: Bool) -> (defaultSelectedIndex: Int, viewModels: [Selection]) { | |||
static func makeViewModels(savedPaymentMethods: [STPPaymentMethod], customerID: String?, showApplePay: Bool, showLink: Bool, allowsSetAsDefaultPM: Bool, elementsSession: STPElementsSession) -> (defaultSelectedIndex: Int, viewModels: [Selection]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to how I don't think we need to pass in the entire ElementsSession, maybe just the customer or default PM. That way we can consolidate all this unwrapping/duplicate code.
// read from back end | ||
if configuration.allowsSetAsDefaultPM, | ||
let customer = elementsSession.customer { | ||
let defaultPaymentMethod = customer.paymentMethods.filter { | ||
$0.stripeId == customer.defaultPaymentMethod | ||
}.first | ||
if let defaultPaymentMethod = defaultPaymentMethod { | ||
return .saved(paymentMethod: defaultPaymentMethod) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is duplicated in a few spots, I think we should consolidate this logic into one spot and pass around the defaultPM or something.
…from elements session logic
I've made a static func getDefaultPaymentMethod(from customer: ElementsCustomer?) -> STPPaymentMethod? in ElementsCustomer.swift |
…odnavigation-flag
…odnavigation-flag
…flag' of github.com:stripe/stripe-ios into joyceqin-remove-alternateupdatepaymentmethodnavigation-flag
…flag' into joyceqin-MOBILESDK-2799
…odnavigation-flag
…vedPaymentMethods_verticalMode
…flag' into joyceqin-MOBILESDK-2799
Okay, I've extracted reading the default payment method from the elements to a function in ElementsCustomer, edited some of the comments, and changed some parameters from ElementsSession to ElementsCustomer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make sure we are also adding tests for this new functionality.
// to test default payment methods reading from back end, hard-code a valid default payment method | ||
// later, when API calls to get and update default payment method are available, that will no longer be needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm, I see what you mean, but don't think we need a comment for that. I don't think anyone else will need this comment as it shouldn't be relevant once this feature is implemented. If you want to leave it let's add a TODO to remove it for yourself.
|
||
static func getDefaultPaymentMethod(from customer: ElementsCustomer?) -> STPPaymentMethod? { | ||
guard let customer = customer else { return nil } | ||
return customer.paymentMethods.first { $0.stripeId == customer.defaultPaymentMethod } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why static? Why not an instance function on ElementsCustomer
with tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't really remember any reasoning of mine, you're right that it should be an instance function on ElementsCustomer
guard configuration.allowsSetAsDefaultPM, | ||
let customer = customer, | ||
let defaultPaymentMethod = customer.defaultPaymentMethod else { | ||
return CustomerPaymentOption.defaultPaymentMethod(for: customerId) | ||
} | ||
|
||
return CustomerPaymentOption.stripeId(defaultPaymentMethod) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior seems inconsistent with other places where we read the default payment method. If the user is opted into allowsSetAsDefaultPM
but the customer does not have a default payment method we will fall through and read it from the stripeId on line 117. In other places we do not fall through. My understanding is that if we are opted into this feature we will use the customer default payment method as the single source of truth for their default. Let's add tests here too.
Correct me if I'm wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why I used guard here instead of if/else because it seems more confusing here, but in this scenario, if the user is opted in but the customer does not have a default, it reads from defaultPaymentMethod on line 114, which returns the local default. I saw the thread with Bella where she said that it should fall back to first saved payment method in the list instead, so I will have to change this to reflect that
// get default payment method from elements session | ||
if configuration.allowsSetAsDefaultPM, | ||
let defaultPaymentMethod = ElementsCustomer.getDefaultPaymentMethod(from: loadResult.elementsSession.customer) { | ||
return .saved(paymentMethod: defaultPaymentMethod) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be in too high in the function, we should use previousPaymentOption
instead of the default on the customer. See line 63 where we try to read the customer default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I didn't consider that the view shouldn't automatically initially select the default because there could have been previous user input. I'll fix that
// get default payment method from elements session | ||
if configuration.allowsSetAsDefaultPM, | ||
let defaultPaymentMethod = ElementsCustomer.getDefaultPaymentMethod(from: elementsSession.customer) { | ||
return .saved(paymentMethod: defaultPaymentMethod) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here as above about being too high in the function, it should not take precedence over previousPaymentOption
. See below for where we look for the saved PM.
…s into joyceqin-MOBILESDK-2799
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there, one small clean up
else { | ||
customerDefault = nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to remove this, customerDefault is implied nil
from it's definition. Same with other similar code paths in the PR.
Summary
Added allowsSetAsDefaultPM flag. Only shown if we're using customer session.
If allowsSetAsDefaultPM is on, then instead of reading from the locally stored default, we read from ElementsSession.
Motivation
MOBILESDK-2799
Testing
customer_session, allowsSetAsDefaultPM on
Hardcoded the value that was "read" from default_payment_method (set to the 4242 Visa stripe id)
After changing the selected payment method and paying with it, upon reload the PaymentSheet default is back to our hardcoded default.
Simulator.Screen.Recording.-.iPhone.16.-.2024-12-02.at.08.05.00.mp4
Changelog