Skip to content

Commit

Permalink
[Android] Replicate other platforms polling logic across the AuthFlow (
Browse files Browse the repository at this point in the history
…#6919)

* [skip ci] Start PR

* Replicate iOS and Web polling logic.

* Updates comments.

* Makes options internal

* Updates tests.

* Allows user retry if polling exceeds max retries.

* Adds tests.

* Updates tests.

* Ktlint.

* Updates changelog.

* Update CHANGELOG.md

Co-authored-by: Till Hellmund <tillh@stripe.com>

---------

Co-authored-by: Till Hellmund <tillh@stripe.com>
  • Loading branch information
carlosmuvi-stripe and tillh-stripe authored Jun 26, 2023
1 parent 950e79a commit 95f91ed
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 139 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## XX.XX.XX - 2023-XX-XX

### Financial Connections
* [CHANGED][6919](https://github.com/stripe/stripe-android/pull/6919) Updated polling options for account retrieval and OAuth results to match other platforms.

## 20.25.7 - 2023-06-20

### Financial Connections
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.stripe.android.financialconnections.domain

import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.model.PartnerAccountsList
import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
import javax.inject.Inject

internal class FetchNetworkedAccounts @Inject constructor(
private val repository: FinancialConnectionsAccountsRepository,
private val configuration: FinancialConnectionsSheet.Configuration
) {

suspend operator fun invoke(
consumerSessionClientSecret: String,
): PartnerAccountsList = repository.getNetworkedAccounts(
clientSecret = configuration.financialConnectionsSessionClientSecret,
consumerSessionClientSecret = consumerSessionClientSecret
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsInstitu
import com.stripe.android.financialconnections.model.LinkAccountSessionPaymentAccount
import com.stripe.android.financialconnections.model.PaymentAccountParams
import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
import com.stripe.android.financialconnections.utils.PollTimingOptions
import com.stripe.android.financialconnections.utils.retryOnException
import com.stripe.android.financialconnections.utils.shouldRetry
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

internal class PollAttachPaymentAccount @Inject constructor(
private val repository: FinancialConnectionsAccountsRepository,
Expand All @@ -25,8 +27,9 @@ internal class PollAttachPaymentAccount @Inject constructor(
params: PaymentAccountParams
): LinkAccountSessionPaymentAccount {
return retryOnException(
times = MAX_TRIES,
delayMilliseconds = POLLING_TIME_MS,
PollTimingOptions(
initialDelayMs = 1.seconds.inWholeMilliseconds,
),
retryCondition = { exception -> exception.shouldRetry }
) {
try {
Expand Down Expand Up @@ -58,11 +61,7 @@ internal class PollAttachPaymentAccount @Inject constructor(
institution = institution,
stripeException = this
)

else -> this
}

private companion object {
private const val POLLING_TIME_MS = 250L
private const val MAX_TRIES = 180
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.exception.AccountLoadError
import com.stripe.android.financialconnections.exception.AccountNoneEligibleForPaymentMethodError
import com.stripe.android.financialconnections.features.common.getBusinessName
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
import com.stripe.android.financialconnections.model.FinancialConnectionsInstitution
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.PartnerAccountsList
import com.stripe.android.financialconnections.repository.FinancialConnectionsAccountsRepository
import com.stripe.android.financialconnections.utils.PollTimingOptions
import com.stripe.android.financialconnections.utils.retryOnException
import com.stripe.android.financialconnections.utils.shouldRetry
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

/**
* Polls accounts from backend after authorization session completes.
*
* Will retry upon 202 backend responses every [POLLING_TIME_MS] up to [MAX_TRIES]
* Will retry upon 202 backend responses.
*/
internal class PollAuthorizationSessionAccounts @Inject constructor(
private val repository: FinancialConnectionsAccountsRepository,
Expand All @@ -27,66 +30,80 @@ internal class PollAuthorizationSessionAccounts @Inject constructor(
suspend operator fun invoke(
canRetry: Boolean,
manifest: FinancialConnectionsSessionManifest
): PartnerAccountsList {
return retryOnException(
times = MAX_TRIES,
delayMilliseconds = POLLING_TIME_MS,
): PartnerAccountsList = try {
val activeAuthSession = requireNotNull(manifest.activeAuthSession)
retryOnException(
PollTimingOptions(
initialDelayMs = activeAuthSession.flow.toPollIntervalMs(),
),
retryCondition = { exception -> exception.shouldRetry }
) {
try {
val authSession = requireNotNull(manifest.activeAuthSession)
val accounts = repository.postAuthorizationSessionAccounts(
clientSecret = configuration.financialConnectionsSessionClientSecret,
sessionId = authSession.id
)
if (accounts.data.isEmpty()) {
throw AccountLoadError(
institution = requireNotNull(manifest.activeInstitution),
allowManualEntry = manifest.allowManualEntry,
canRetry = canRetry,
stripeException = APIException()
)
} else {
accounts
}
} catch (@Suppress("SwallowedException") e: StripeException) {
throw e.toDomainException(
institution = manifest.activeInstitution,
businessName = manifest.getBusinessName(),
val accounts = repository.postAuthorizationSessionAccounts(
clientSecret = configuration.financialConnectionsSessionClientSecret,
sessionId = activeAuthSession.id
)
if (accounts.data.isEmpty()) {
throw AccountLoadError(
institution = requireNotNull(manifest.activeInstitution),
allowManualEntry = manifest.allowManualEntry,
canRetry = canRetry,
allowManualEntry = manifest.allowManualEntry
stripeException = APIException()
)
} else {
accounts
}
}
} catch (@Suppress("SwallowedException") e: StripeException) {
throw e.toDomainException(
institution = manifest.activeInstitution,
businessName = manifest.getBusinessName(),
canRetry = canRetry,
allowManualEntry = manifest.allowManualEntry
)
}
}

private fun StripeException.toDomainException(
institution: FinancialConnectionsInstitution?,
businessName: String?,
canRetry: Boolean,
allowManualEntry: Boolean
): StripeException =
when {
institution == null -> this
stripeError?.extraFields?.get("reason") == "no_supported_payment_method_type_accounts_found" ->
AccountNoneEligibleForPaymentMethodError(
accountsCount = stripeError?.extraFields?.get("total_accounts_count")?.toInt()
?: 0,
institution = institution,
merchantName = businessName ?: "",
stripeException = this
)

else -> AccountLoadError(
allowManualEntry = allowManualEntry,
private fun StripeException.toDomainException(
institution: FinancialConnectionsInstitution?,
businessName: String?,
canRetry: Boolean,
allowManualEntry: Boolean
): StripeException =
when {
institution == null -> this
stripeError?.extraFields?.get("reason") == "no_supported_payment_method_type_accounts_found" ->
AccountNoneEligibleForPaymentMethodError(
accountsCount = stripeError?.extraFields?.get("total_accounts_count")?.toInt()
?: 0,
institution = institution,
canRetry = canRetry,
merchantName = businessName ?: "",
stripeException = this
)

else -> AccountLoadError(
allowManualEntry = allowManualEntry,
institution = institution,
canRetry = canRetry,
stripeException = this
)
}

private fun FinancialConnectionsAuthorizationSession.Flow?.toPollIntervalMs(): Long {
val defaultInitialPollDelay: Long = 1.75.seconds.inWholeMilliseconds
return when (this) {
FinancialConnectionsAuthorizationSession.Flow.TESTMODE,
FinancialConnectionsAuthorizationSession.Flow.TESTMODE_OAUTH,
FinancialConnectionsAuthorizationSession.Flow.TESTMODE_OAUTH_WEBVIEW,
FinancialConnectionsAuthorizationSession.Flow.FINICITY_CONNECT_V2_LITE -> {
// Post auth flow, Finicity non-OAuth account retrieval latency is extremely quick - p90 < 1sec.
0
}

FinancialConnectionsAuthorizationSession.Flow.MX_CONNECT -> {
// 10 account retrieval latency on MX non-OAuth sessions is currently 460 ms
0.5.seconds.inWholeMilliseconds
}

private companion object {
private const val POLLING_TIME_MS = 2000L
private const val MAX_TRIES = 10
else -> defaultInitialPollDelay
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheet
import com.stripe.android.financialconnections.model.FinancialConnectionsAuthorizationSession
import com.stripe.android.financialconnections.model.MixedOAuthParams
import com.stripe.android.financialconnections.repository.FinancialConnectionsRepository
import com.stripe.android.financialconnections.utils.PollTimingOptions
import com.stripe.android.financialconnections.utils.retryOnException
import com.stripe.android.financialconnections.utils.shouldRetry
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

/**
* Polls OAuth results from backend after user finishes authorization on web browser.
*
* Will retry upon 202 backend responses every [POLLING_TIME_MS] up to [MAX_TRIES]
* Will retry upon 202 backend responses.
*/
internal class PollAuthorizationSessionOAuthResults @Inject constructor(
private val repository: FinancialConnectionsRepository,
Expand All @@ -22,8 +24,11 @@ internal class PollAuthorizationSessionOAuthResults @Inject constructor(
session: FinancialConnectionsAuthorizationSession
): MixedOAuthParams {
return retryOnException(
times = MAX_TRIES,
delayMilliseconds = POLLING_TIME_MS,
PollTimingOptions(
initialDelayMs = 0,
maxNumberOfRetries = 300, // Stripe.js has 600 second timeout, 600 / 2 = 300 retries
retryInterval = 2.seconds.inWholeMilliseconds
),
retryCondition = { exception -> exception.shouldRetry }
) {
repository.postAuthorizationSessionOAuthResults(
Expand All @@ -32,9 +37,4 @@ internal class PollAuthorizationSessionOAuthResults @Inject constructor(
)
}
}

private companion object {
private const val POLLING_TIME_MS = 2000L
private const val MAX_TRIES = 300
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsEve
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.ClickLearnMoreDataAccess
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.Error
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.PaneLoaded
import com.stripe.android.financialconnections.domain.FetchNetworkedAccounts
import com.stripe.android.financialconnections.domain.GetCachedConsumerSession
import com.stripe.android.financialconnections.domain.GetManifest
import com.stripe.android.financialconnections.domain.GoNext
import com.stripe.android.financialconnections.domain.PollNetworkedAccounts
import com.stripe.android.financialconnections.domain.SelectNetworkedAccount
import com.stripe.android.financialconnections.domain.UpdateCachedAccounts
import com.stripe.android.financialconnections.domain.UpdateLocalManifest
Expand All @@ -32,7 +32,7 @@ internal class LinkAccountPickerViewModel @Inject constructor(
initialState: LinkAccountPickerState,
private val eventTracker: FinancialConnectionsAnalyticsTracker,
private val getCachedConsumerSession: GetCachedConsumerSession,
private val pollNetworkedAccounts: PollNetworkedAccounts,
private val fetchNetworkedAccounts: FetchNetworkedAccounts,
private val selectNetworkedAccount: SelectNetworkedAccount,
private val updateLocalManifest: UpdateLocalManifest,
private val updateCachedAccounts: UpdateCachedAccounts,
Expand All @@ -53,7 +53,7 @@ internal class LinkAccountPickerViewModel @Inject constructor(
dataPolicyUrl = FinancialConnectionsUrlResolver.getDataPolicyUrl(manifest)
)
val consumerSession = requireNotNull(getCachedConsumerSession())
val accountsResponse = pollNetworkedAccounts(consumerSession.clientSecret)
val accountsResponse = fetchNetworkedAccounts(consumerSession.clientSecret)
val accounts = accountsResponse
.data
// Override allow selection to disable disconnected accounts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import com.stripe.android.financialconnections.analytics.FinancialConnectionsEve
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.VerificationSuccess
import com.stripe.android.financialconnections.analytics.FinancialConnectionsEvent.VerificationSuccessNoAccounts
import com.stripe.android.financialconnections.domain.ConfirmVerification
import com.stripe.android.financialconnections.domain.FetchNetworkedAccounts
import com.stripe.android.financialconnections.domain.GetManifest
import com.stripe.android.financialconnections.domain.GoNext
import com.stripe.android.financialconnections.domain.LookupConsumerAndStartVerification
import com.stripe.android.financialconnections.domain.MarkLinkVerified
import com.stripe.android.financialconnections.domain.PollNetworkedAccounts
import com.stripe.android.financialconnections.features.networkinglinkverification.NetworkingLinkVerificationState.Payload
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest
import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest.Pane
Expand All @@ -45,7 +45,7 @@ internal class NetworkingLinkVerificationViewModel @Inject constructor(
private val getManifest: GetManifest,
private val confirmVerification: ConfirmVerification,
private val markLinkVerified: MarkLinkVerified,
private val pollNetworkedAccounts: PollNetworkedAccounts,
private val fetchNetworkedAccounts: FetchNetworkedAccounts,
private val goNext: GoNext,
private val analyticsTracker: FinancialConnectionsAnalyticsTracker,
private val lookupConsumerAndStartVerification: LookupConsumerAndStartVerification,
Expand Down Expand Up @@ -119,7 +119,7 @@ internal class NetworkingLinkVerificationViewModel @Inject constructor(
verificationCode = otp
)
val updatedManifest = markLinkVerified()
runCatching { pollNetworkedAccounts(payload.consumerSessionClientSecret) }
runCatching { fetchNetworkedAccounts(payload.consumerSessionClientSecret) }
.fold(
onSuccess = { onNetworkedAccountsSuccess(it, updatedManifest) },
onFailure = { onNetworkedAccountsFailed(it, updatedManifest) }
Expand Down
Loading

0 comments on commit 95f91ed

Please sign in to comment.