-
Notifications
You must be signed in to change notification settings - Fork 658
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
Fix odd behavior in Stripe.retrievePossibleBrands #6977
Conversation
|
||
val accountRanges = | ||
stripeRepository.getCardMetadata(bin, requestOptions)?.accountRanges.orEmpty() | ||
cardAccountRangeStore.save(bin, accountRanges) |
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 no longer store any response, as these could include null
responses due to network errors. Instead, the repository will decide whether to store a result.
accountRanges | ||
if (!accountRanges.isNullOrEmpty()) { | ||
val hasMatch = accountRanges.any { it.binRange.matches(cardNumber) } | ||
if (!hasMatch && cardNumber.isValidLuhn) { |
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.
Re-adding the hasMatch
check that was removed in https://github.com/stripe/stripe-android/pull/6335/files#diff-d6f7381dc56080adb96df46f224f39adff555b81bdff2952944a80530f1d9e03L39.
cc @jameswoo-stripe @davidme-stripe @wooj-stripe: I’m not fully sure if this implementation is correct. It seems more correct than before, but iOS does have some additional logic that I don’t understand. Any thoughts?
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.
Sorry, I don't remember why I removed that piece of logic :( It does seem odd that it was removed!
@@ -77,7 +75,7 @@ class CardAccountRangeServiceTest { | |||
} else { | |||
never() | |||
} | |||
).getAccountRange(cardNumber) | |||
).getAccountRanges(cardNumber) |
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.
The implementation changed, so the test needs to change 😕
if (!ranges.isNullOrEmpty()) { | ||
store.save(bin, ranges) | ||
} | ||
|
||
return ranges?.takeIf { it.isNotEmpty() } ?: staticSource.getAccountRanges(cardNumber) |
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 only save the response if we actually get a non-empty one. The backend service returns an empty list for some valid BINs (🤷) and I assume we don’t want to serve those but would rather fall back to the static ranges.
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 think that the remove backend service is the source of truth, we probably should not rely on our static list. I think as long as the response is successful, we should use that data.
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 is inconsistent with what we’re doing in PaymentSheet then, where we take the static ranges into account (cc @davidme-stripe and @wooj-stripe for help)
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.
The BIN data is often wrong, even in cases where it doesn't return an empty response. (See RUN_MOBILESDK-2135 for more discussion on this.)
On iOS in the card field, we only fetch it for UnionPay BINs as they could be 19 digit cards (81/62). For this possibleCardBrands
API, we actually maintain a separate cache, as we don't want to pollute the card text field's cache with bad data. Would that explain the difference in behavior you're seeing?
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.
@davidme-stripe We seem to have the same logic on Android in CardAccountRangeService
— see shouldQueryRepository
, which only returns true for unknown brands and UnionPay. That service is used in the card field and PaymentSheet, but not for possibleCardBrands
. Should it be?
@jameswoo-stripe We already take the static ranges into account right now, but only on the very first call for a given prefix. Afterwards, the fact that we store things on the device makes us return an empty list instead of results from the static ranges.
private const val VERSION = 2 | ||
private const val PREF_FILE = "InMemoryCardAccountRangeSource.Store.$VERSION" |
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.
Updating the key here so that we can ”force-refresh”. I’m doing this because we might already be in a corrupted state for existing users.
Diffuse output:
APK
DEX
|
ee863ed
to
6f5c77a
Compare
@@ -24,14 +24,12 @@ import kotlin.test.Test | |||
|
|||
@RunWith(RobolectricTestRunner::class) | |||
internal class RemoteCardAccountRangeSourceTest { | |||
private val cardAccountRangeStore = mock<CardAccountRangeStore>() |
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.
Should we have a test that does check if the data was saved?
if (!ranges.isNullOrEmpty()) { | ||
store.save(bin, ranges) | ||
} | ||
|
||
return ranges?.takeIf { it.isNotEmpty() } ?: staticSource.getAccountRanges(cardNumber) |
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 think that the remove backend service is the source of truth, we probably should not rely on our static list. I think as long as the response is successful, we should use that data.
accountRanges | ||
if (!accountRanges.isNullOrEmpty()) { | ||
val hasMatch = accountRanges.any { it.binRange.matches(cardNumber) } | ||
if (!hasMatch && cardNumber.isValidLuhn) { |
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.
Sorry, I don't remember why I removed that piece of logic :( It does seem odd that it was removed!
6f5c77a
to
e9e8972
Compare
@jameswoo-stripe I reduced the diff so that my changes are more clearly visible. |
79a91c1
to
8a1554c
Compare
7554fa8
to
1d24b54
Compare
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.
Do we have tests to verify we're using the stored account ranges?
payments-core/src/main/java/com/stripe/android/cards/RemoteCardAccountRangeSource.kt
Show resolved
Hide resolved
1d24b54
to
9eea0bd
Compare
Store an empty response to avoid queries, but fall back to static ranges if the stored/fetched response is empty.
208051a
to
d9722cb
Compare
* Fix odd behavior in Stripe.possibleCardBrands * Update tests * Reduce queries for card account ranges Store an empty response to avoid queries, but fall back to static ranges if the stored/fetched response is empty. * Fix test * Update remote source test for new behavior * Make minor tweaks * Add changelog entry * Add more tests * Remove method that’s now unused
Summary
This pull request fixes some odd behavior that affects
stripe.retrievePossibleBrands()
.In
RemoteCardAccountRangeSource
, we stored any response from the backend in our persistent storage. This is problematic for two reasons: Firstly, the backend returns empty ranges for (seemingly?) valid BINs. Secondly, we don’t filter out invalid responses due to network or parsing errors. In both cases, we’d store an empty response on the device, which we’d continue to serve to callers into eternity, as we never clear the local storage. Now, we will fall back to the static ranges if the response from the local or remote data sources are empty.Also, the conditions under which we called
onCardMetadataMissingRange()
was wrong, leading us to send way too many events.Motivation
Correctly working card brand functionality in preparation for CBC.
Testing
Screenshots
Changelog
[FIXED] Fixed an issue where
Stripe.retrievePossibleBrands()
returned incorrect results.