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

Add fraud detection in FC module #9372

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import androidx.lifecycle.SavedStateHandle
import com.stripe.android.core.ApiVersion
import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.core.networking.StripeNetworkClient
import com.stripe.android.core.version.StripeSdkVersion
Expand Down Expand Up @@ -125,6 +126,7 @@ internal interface FinancialConnectionsSheetNativeModule {
locale: Locale?,
logger: Logger,
isLinkWithStripe: IsLinkWithStripe,
fraudDetectionDataRepository: FraudDetectionDataRepository,
) = FinancialConnectionsConsumerSessionRepository(
financialConnectionsConsumersApiService = financialConnectionsConsumersApiService,
provideApiRequestOptions = provideApiRequestOptions,
Expand All @@ -133,6 +135,7 @@ internal interface FinancialConnectionsSheetNativeModule {
locale = locale ?: Locale.getDefault(),
logger = logger,
isLinkWithStripe = isLinkWithStripe,
fraudDetectionDataRepository = fraudDetectionDataRepository,
)

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stripe.android.financialconnections.di
import android.app.Application
import com.stripe.android.core.ApiVersion
import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
import com.stripe.android.core.networking.AnalyticsRequestExecutor
Expand Down Expand Up @@ -33,6 +34,7 @@ import com.stripe.android.financialconnections.repository.ConsumerSessionReposit
import com.stripe.android.financialconnections.repository.FinancialConnectionsRepository
import com.stripe.android.financialconnections.repository.FinancialConnectionsRepositoryImpl
import com.stripe.android.financialconnections.repository.RealConsumerSessionRepository
import com.stripe.android.financialconnections.utils.DefaultFraudDetectionDataRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -183,5 +185,12 @@ internal interface FinancialConnectionsSheetSharedModule {
internal fun providesIoDispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}

@Provides
internal fun provideFraudDetectionDataRepository(
application: Application,
): FraudDetectionDataRepository {
return DefaultFraudDetectionDataRepository(application)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.financialconnections.repository

import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.financialconnections.domain.IsLinkWithStripe
import com.stripe.android.financialconnections.repository.api.FinancialConnectionsConsumersApiService
import com.stripe.android.financialconnections.repository.api.ProvideApiRequestOptions
Expand Down Expand Up @@ -72,6 +73,7 @@ internal interface FinancialConnectionsConsumerSessionRepository {
locale: Locale?,
logger: Logger,
isLinkWithStripe: IsLinkWithStripe,
fraudDetectionDataRepository: FraudDetectionDataRepository,
): FinancialConnectionsConsumerSessionRepository =
FinancialConnectionsConsumerSessionRepositoryImpl(
consumersApiService = consumersApiService,
Expand All @@ -80,6 +82,7 @@ internal interface FinancialConnectionsConsumerSessionRepository {
consumerSessionRepository = consumerSessionRepository,
locale = locale,
logger = logger,
fraudDetectionDataRepository = fraudDetectionDataRepository,
isLinkWithStripe = isLinkWithStripe,
)
}
Expand All @@ -92,6 +95,7 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
private val provideApiRequestOptions: ProvideApiRequestOptions,
private val locale: Locale?,
private val logger: Logger,
private val fraudDetectionDataRepository: FraudDetectionDataRepository,
isLinkWithStripe: IsLinkWithStripe,
) : FinancialConnectionsConsumerSessionRepository {

Expand All @@ -103,6 +107,10 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
"android_connections"
}

init {
fraudDetectionDataRepository.refresh()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Similar approach to the Payments SDK: Refresh at launch, then use cached values in the request.

We’re only refreshing in the FinancialConnectionsConsumerSessionRepository for now and not FinancialConnectionsRepository, to avoid double work. We could move this into an initializer and run it just once, regardless of which objects get instantiated.

}

override suspend fun getCachedConsumerSession(): CachedConsumerSession? = mutex.withLock {
consumerSessionRepository.provideConsumerSession()
}
Expand Down Expand Up @@ -201,12 +209,15 @@ private class FinancialConnectionsConsumerSessionRepositoryImpl(
consumerSessionClientSecret: String,
expectedPaymentMethodType: String
): SharePaymentDetails {
val fraudDetectionData = fraudDetectionDataRepository.getCached()?.params.orEmpty()

return consumersApiService.sharePaymentDetails(
consumerSessionClientSecret = consumerSessionClientSecret,
paymentDetailsId = paymentDetailsId,
expectedPaymentMethodType = expectedPaymentMethodType,
requestSurface = requestSurface,
requestOptions = provideApiRequestOptions(useConsumerPublishableKey = false),
extraParams = fraudDetectionData,
).getOrThrow()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.stripe.android.core.exception.APIConnectionException
import com.stripe.android.core.exception.APIException
import com.stripe.android.core.exception.AuthenticationException
import com.stripe.android.core.exception.InvalidRequestException
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.financialconnections.model.FinancialConnectionsAccountList
import com.stripe.android.financialconnections.model.FinancialConnectionsSession
Expand Down Expand Up @@ -62,7 +63,8 @@ internal interface FinancialConnectionsRepository {
internal class FinancialConnectionsRepositoryImpl @Inject constructor(
private val requestExecutor: FinancialConnectionsRequestExecutor,
private val provideApiRequestOptions: ProvideApiRequestOptions,
private val apiRequestFactory: ApiRequest.Factory
private val fraudDetectionDataRepository: FraudDetectionDataRepository,
private val apiRequestFactory: ApiRequest.Factory,
) : FinancialConnectionsRepository {

override suspend fun getFinancialConnectionsAccounts(
Expand Down Expand Up @@ -147,10 +149,12 @@ internal class FinancialConnectionsRepositoryImpl @Inject constructor(
),
)

val fraudDetectionParams = fraudDetectionDataRepository.getCached()?.params.orEmpty()

val request = apiRequestFactory.createPost(
url = paymentMethodsUrl,
options = provideApiRequestOptions(useConsumerPublishableKey = false),
params = params,
params = params + fraudDetectionParams,
)

return requestExecutor.execute(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stripe.android.financialconnections.utils

import android.app.Application
import com.stripe.android.core.frauddetection.DefaultFraudDetectionDataRepository
import com.stripe.android.core.frauddetection.DefaultFraudDetectionDataRequestFactory
import com.stripe.android.core.frauddetection.DefaultFraudDetectionDataStore
import com.stripe.android.core.networking.DefaultStripeNetworkClient
import kotlinx.coroutines.Dispatchers

internal fun DefaultFraudDetectionDataRepository(
application: Application,
): DefaultFraudDetectionDataRepository {
val workContext = Dispatchers.IO

return DefaultFraudDetectionDataRepository(
localStore = DefaultFraudDetectionDataStore(application, workContext),
fraudDetectionDataRequestFactory = DefaultFraudDetectionDataRequestFactory(application),
stripeNetworkClient = DefaultStripeNetworkClient(workContext = workContext),
errorReporter = { /* No-op */ },
workContext = workContext,
fraudDetectionEnabledProvider = { true },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.stripe.android.financialconnections.repository
import androidx.lifecycle.SavedStateHandle
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionData
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.financialconnections.ApiKeyFixtures
import com.stripe.android.financialconnections.ApiKeyFixtures.consumerSession
Expand All @@ -15,6 +17,7 @@ import com.stripe.android.model.ConsumerSession.VerificationSession.SessionType
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.CustomEmailType
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.VerificationType
import com.stripe.android.repository.ConsumersApiService
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -23,6 +26,7 @@ import org.junit.Test
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.Locale
Expand All @@ -39,6 +43,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
private val logger: Logger = mock()
private val locale: Locale = Locale.getDefault()
private val consumerSessionRepository = RealConsumerSessionRepository(SavedStateHandle())
private val fraudDetectionDataRepository = mock<FraudDetectionDataRepository>()

private fun buildRepository(
isInstantDebits: Boolean = false
Expand All @@ -50,6 +55,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
locale = locale,
logger = logger,
isLinkWithStripe = { isInstantDebits },
fraudDetectionDataRepository = fraudDetectionDataRepository,
)

@Test
Expand Down Expand Up @@ -324,4 +330,42 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
requestOptions = anyOrNull(),
)
}

@Test
fun `Sends fraud detection data when sharing PaymentDetails`() = runTest {
val consumerSessionClientSecret = "clientSecret"
val repository = buildRepository()

val fraudParams = FraudDetectionData(
guid = "guid_1234",
muid = "muid_1234",
sid = "sid_1234",
timestamp = 1234567890L,
)

whenever(fraudDetectionDataRepository.getCached()).thenReturn(fraudParams)

whenever(
consumersApiService.sharePaymentDetails(
consumerSessionClientSecret = anyOrNull(),
paymentDetailsId = anyOrNull(),
expectedPaymentMethodType = anyOrNull(),
requestSurface = anyOrNull(),
requestOptions = anyOrNull(),
extraParams = eq(fraudParams.params),
)
).thenReturn(
Result.success(
SharePaymentDetails(paymentMethodId = "pm_123")
)
)

repository.sharePaymentDetails(
consumerSessionClientSecret = consumerSessionClientSecret,
paymentDetailsId = "pd_123",
expectedPaymentMethodType = "card",
)

verify(fraudDetectionDataRepository, never()).getLatest()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.stripe.android.financialconnections.repository

import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.Logger
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.core.networking.StripeNetworkClient
import com.stripe.android.core.networking.StripeResponse
Expand All @@ -24,6 +25,7 @@ class FinancialConnectionsRepositoryImplTest {

private val mockStripeNetworkClient = mock<StripeNetworkClient>()
private val apiRequestFactory = ApiRequest.Factory()
private val fraudDetectionDataRepository = mock<FraudDetectionDataRepository>()

private val financialConnectionsRepositoryImpl = FinancialConnectionsRepositoryImpl(
requestExecutor = FinancialConnectionsRequestExecutor(
Expand All @@ -36,6 +38,7 @@ class FinancialConnectionsRepositoryImplTest {
ApiRequest.Options(ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY)
},
apiRequestFactory = apiRequestFactory,
fraudDetectionDataRepository = fraudDetectionDataRepository,
)

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.stripe.android.core.exception.PermissionException
import com.stripe.android.core.exception.RateLimitException
import com.stripe.android.core.exception.StripeException
import com.stripe.android.core.exception.safeAnalyticsMessage
import com.stripe.android.core.frauddetection.DefaultFraudDetectionDataRepository
import com.stripe.android.core.frauddetection.FraudDetectionData
import com.stripe.android.core.frauddetection.FraudDetectionDataParamsUtils
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ interface ConsumersApiService {
expectedPaymentMethodType: String,
requestSurface: String,
requestOptions: ApiRequest.Options,
extraParams: Map<String, Any?>,
): Result<SharePaymentDetails>
}

Expand Down Expand Up @@ -277,7 +278,8 @@ class ConsumersApiServiceImpl(
paymentDetailsId: String,
expectedPaymentMethodType: String,
requestSurface: String,
requestOptions: ApiRequest.Options
requestOptions: ApiRequest.Options,
extraParams: Map<String, Any?>,
): Result<SharePaymentDetails> {
return executeRequestWithResultParser(
stripeErrorJsonParser = stripeErrorJsonParser,
Expand All @@ -292,7 +294,7 @@ class ConsumersApiServiceImpl(
"credentials" to mapOf(
"consumer_session_client_secret" to consumerSessionClientSecret
),
)
) + extraParams,
),
responseJsonParser = SharePaymentDetailsJsonParser,
)
Expand Down
Loading