Skip to content

Commit

Permalink
Add fraud detection in FC module
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed Oct 1, 2024
1 parent f5ca7d5 commit a942e43
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 5 deletions.
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.FinancialConnectionsFraudDetectionRepositoryFactory
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 FinancialConnectionsFraudDetectionRepositoryFactory.create(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()
}

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.getLatest()?.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.getLatest()?.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,25 @@
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.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.networking.DefaultStripeNetworkClient
import kotlinx.coroutines.Dispatchers

internal object FinancialConnectionsFraudDetectionRepositoryFactory {

fun create(application: Application): FraudDetectionDataRepository {
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 @@ -39,6 +42,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 +54,7 @@ class FinancialConnectionsConsumerSessionRepositoryImplTest {
locale = locale,
logger = logger,
isLinkWithStripe = { isInstantDebits },
fraudDetectionDataRepository = fraudDetectionDataRepository,
)

@Test
Expand Down Expand Up @@ -324,4 +329,40 @@ 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",
)
}
}
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

0 comments on commit a942e43

Please sign in to comment.