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

[Android] Replicate other platforms polling logic across the AuthFlow #6919

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
) {
Comment on lines +8 to +11
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just renamed from PollNetworkedAccounts. No polling needed here.


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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

try now wraps the retryOnException execution, so that toDomainException also maps retry exceptions.

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
Comment on lines +91 to +107
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

replicated from iOS. Each flow has an optimal initial delay.

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