From 6a0280d67c133ad78900ea8fad50436bd159dff4 Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Tue, 18 May 2021 22:39:32 -0400 Subject: [PATCH 1/6] Add Stripe.createRadarSession() API binding Add support for creating a Radar Session through `/v1/radar/sessions`. `Stripe.advancedFraudSignalsEnabled` must be set to `true` (i.e. the default value) to use this method. --- .../android/FingerprintDataRepository.kt | 72 +++++++++++++------ .../stripe/android/FingerprintDataStore.kt | 13 ++-- .../stripe/android/PaymentConfiguration.kt | 4 +- .../main/java/com/stripe/android/Stripe.kt | 24 +++++++ .../main/java/com/stripe/android/StripeKtx.kt | 26 +++++++ .../com/stripe/android/model/RadarSession.kt | 8 +++ .../model/parsers/RadarSessionJsonParser.kt | 17 +++++ .../android/networking/AnalyticsEvent.kt | 2 + .../android/networking/StripeApiRepository.kt | 44 +++++++++++- .../android/networking/StripeRepository.kt | 5 ++ .../android/FakeFingerprintDataRepository.kt | 21 ++++-- .../android/FingerprintDataRepositoryTest.kt | 4 +- .../com/stripe/android/StripeEndToEndTest.kt | 7 ++ .../parsers/RadarSessionJsonParserTest.kt | 27 +++++++ .../networking/AbsFakeStripeRepository.kt | 5 ++ .../networking/StripeApiRepositoryTest.kt | 62 +++++++++++++++- 16 files changed, 298 insertions(+), 43 deletions(-) create mode 100644 stripe/src/main/java/com/stripe/android/model/RadarSession.kt create mode 100644 stripe/src/main/java/com/stripe/android/model/parsers/RadarSessionJsonParser.kt create mode 100644 stripe/src/test/java/com/stripe/android/model/parsers/RadarSessionJsonParserTest.kt diff --git a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt index 9397bce63c8..a9a024b834a 100644 --- a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt +++ b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt @@ -1,17 +1,37 @@ package com.stripe.android import android.content.Context +import androidx.annotation.UiThread +import androidx.annotation.WorkerThread import com.stripe.android.networking.FingerprintRequestExecutor import com.stripe.android.networking.FingerprintRequestFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Calendar import kotlin.coroutines.CoroutineContext internal interface FingerprintDataRepository { + @UiThread fun refresh() - fun get(): FingerprintData? + + /** + * Get the cached [FingerprintData]. This is not a blocking request. + */ + @UiThread + fun getCached(): FingerprintData? + + /** + * Get the latest [FingerprintData]. This is a blocking request. + * + * 1. From [FingerprintDataStore] if that value is not expired. + * 2. Otherwise, from the network. + */ + @WorkerThread + suspend fun getLatest(): FingerprintData? + + @UiThread fun save(fingerprintData: FingerprintData) class Default( @@ -27,41 +47,47 @@ internal interface FingerprintDataRepository { Calendar.getInstance().timeInMillis } + @JvmOverloads constructor( - context: Context + context: Context, + workContext: CoroutineContext = Dispatchers.IO ) : this( - localStore = FingerprintDataStore.Default(context), + localStore = FingerprintDataStore.Default(context, workContext), fingerprintRequestFactory = FingerprintRequestFactory.Default(context), - workContext = Dispatchers.IO + workContext = workContext ) override fun refresh() { if (Stripe.advancedFraudSignalsEnabled) { CoroutineScope(workContext).launch { - localStore.get().let { localFingerprintData -> - if (localFingerprintData == null || - localFingerprintData.isExpired(timestampSupplier()) - ) { - fingerprintRequestExecutor.execute( - request = fingerprintRequestFactory.create( - localFingerprintData - ) - ) - } else { + getLatest() + } + } + } + + override suspend fun getLatest() = withContext(workContext) { + val latestFingerprintData = localStore.get().let { localFingerprintData -> + if (localFingerprintData == null || + localFingerprintData.isExpired(timestampSupplier()) + ) { + fingerprintRequestExecutor.execute( + request = fingerprintRequestFactory.create( localFingerprintData - } - }.let { fingerprintData -> - if (cachedFingerprintData != fingerprintData) { - fingerprintData?.let { - save(it) - } - } - } + ) + ) + } else { + localFingerprintData } } + + if (cachedFingerprintData != latestFingerprintData) { + latestFingerprintData?.let(::save) + } + + latestFingerprintData } - override fun get(): FingerprintData? { + override fun getCached(): FingerprintData? { return cachedFingerprintData.takeIf { Stripe.advancedFraudSignalsEnabled } diff --git a/stripe/src/main/java/com/stripe/android/FingerprintDataStore.kt b/stripe/src/main/java/com/stripe/android/FingerprintDataStore.kt index 399380fd8c5..511b156c45e 100644 --- a/stripe/src/main/java/com/stripe/android/FingerprintDataStore.kt +++ b/stripe/src/main/java/com/stripe/android/FingerprintDataStore.kt @@ -1,11 +1,12 @@ package com.stripe.android import android.content.Context +import androidx.core.content.edit import com.stripe.android.model.parsers.FingerprintDataJsonParser -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import kotlin.coroutines.CoroutineContext internal interface FingerprintDataStore { suspend fun get(): FingerprintData? @@ -13,7 +14,7 @@ internal interface FingerprintDataStore { class Default( context: Context, - private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO + private val workContext: CoroutineContext = Dispatchers.IO ) : FingerprintDataStore { private val prefs by lazy { context.getSharedPreferences( @@ -22,7 +23,7 @@ internal interface FingerprintDataStore { ) } - override suspend fun get() = withContext(coroutineDispatcher) { + override suspend fun get() = withContext(workContext) { runCatching { val json = JSONObject(prefs.getString(KEY_DATA, null).orEmpty()) val timestampSupplier = { @@ -33,9 +34,9 @@ internal interface FingerprintDataStore { } override fun save(fingerprintData: FingerprintData) { - prefs.edit() - .putString(KEY_DATA, fingerprintData.toJson().toString()) - .apply() + prefs.edit { + putString(KEY_DATA, fingerprintData.toJson().toString()) + } } private companion object { diff --git a/stripe/src/main/java/com/stripe/android/PaymentConfiguration.kt b/stripe/src/main/java/com/stripe/android/PaymentConfiguration.kt index 81bfa3c1aaa..010d7d731ef 100644 --- a/stripe/src/main/java/com/stripe/android/PaymentConfiguration.kt +++ b/stripe/src/main/java/com/stripe/android/PaymentConfiguration.kt @@ -18,12 +18,12 @@ data class PaymentConfiguration internal constructor( /** * Manages saving and loading [PaymentConfiguration] data to SharedPreferences. */ - private class Store internal constructor(context: Context) { + private class Store(context: Context) { private val prefs: SharedPreferences = context.applicationContext.getSharedPreferences(NAME, 0) @JvmSynthetic - internal fun save( + fun save( publishableKey: String, stripeAccountId: String? ) { diff --git a/stripe/src/main/java/com/stripe/android/Stripe.kt b/stripe/src/main/java/com/stripe/android/Stripe.kt index e3f4ca6c0c3..48258449f45 100644 --- a/stripe/src/main/java/com/stripe/android/Stripe.kt +++ b/stripe/src/main/java/com/stripe/android/Stripe.kt @@ -29,6 +29,7 @@ import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.PersonTokenParams import com.stripe.android.model.PiiTokenParams +import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source import com.stripe.android.model.SourceParams @@ -2006,6 +2007,29 @@ class Stripe internal constructor( } } + /** + * Create a Radar Session asynchronously + * + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * @param callback a [ApiResultCallback] to receive the result or error + */ + @UiThread + @JvmOverloads + fun createRadarSession( + stripeAccountId: String? = this.stripeAccountId, + callback: ApiResultCallback + ) { + executeAsync(callback) { + stripeRepository.createRadarSession( + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId + ) + ) + } + } + private fun executeAsync( callback: ApiResultCallback, apiMethod: suspend () -> T? diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index bc69875f084..6101a7dca9e 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -19,6 +19,7 @@ import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.model.PersonTokenParams import com.stripe.android.model.PiiTokenParams +import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source import com.stripe.android.model.SourceParams @@ -422,6 +423,31 @@ suspend fun Stripe.createFile( ) } +/** + * Create a Radar Session. + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.createRadarSession(): RadarSession { + return runApiRequest { + stripeRepository.createRadarSession( + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId + ) + ) + } +} + /** * Retrieve a [PaymentIntent] from a coroutine. * diff --git a/stripe/src/main/java/com/stripe/android/model/RadarSession.kt b/stripe/src/main/java/com/stripe/android/model/RadarSession.kt new file mode 100644 index 00000000000..6d88b37f243 --- /dev/null +++ b/stripe/src/main/java/com/stripe/android/model/RadarSession.kt @@ -0,0 +1,8 @@ +package com.stripe.android.model + +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RadarSession( + val id: String +) : StripeModel diff --git a/stripe/src/main/java/com/stripe/android/model/parsers/RadarSessionJsonParser.kt b/stripe/src/main/java/com/stripe/android/model/parsers/RadarSessionJsonParser.kt new file mode 100644 index 00000000000..1de7d944b7b --- /dev/null +++ b/stripe/src/main/java/com/stripe/android/model/parsers/RadarSessionJsonParser.kt @@ -0,0 +1,17 @@ +package com.stripe.android.model.parsers + +import com.stripe.android.model.RadarSession +import com.stripe.android.model.StripeJsonUtils +import org.json.JSONObject + +internal class RadarSessionJsonParser : ModelJsonParser { + override fun parse(json: JSONObject): RadarSession? { + return StripeJsonUtils.optString(json, FIELD_ID)?.let { + RadarSession(it) + } + } + + private companion object { + private const val FIELD_ID = "id" + } +} diff --git a/stripe/src/main/java/com/stripe/android/networking/AnalyticsEvent.kt b/stripe/src/main/java/com/stripe/android/networking/AnalyticsEvent.kt index 44f726df15c..a44c469a75e 100644 --- a/stripe/src/main/java/com/stripe/android/networking/AnalyticsEvent.kt +++ b/stripe/src/main/java/com/stripe/android/networking/AnalyticsEvent.kt @@ -79,6 +79,8 @@ internal enum class AnalyticsEvent(internal val code: String) { AuthSourceRedirect("auth_source_redirect"), AuthSourceResult("auth_source_result"), + RadarSessionCreate("radar_session_create"), + CardMetadataPublishableKeyAvailable("card_metadata_pk_available"), CardMetadataPublishableKeyUnavailable("card_metadata_pk_unavailable"), diff --git a/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt b/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt index 0ad2712a105..9b60b56686c 100644 --- a/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt +++ b/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt @@ -17,6 +17,7 @@ import com.stripe.android.exception.CardException import com.stripe.android.exception.InvalidRequestException import com.stripe.android.exception.PermissionException import com.stripe.android.exception.RateLimitException +import com.stripe.android.exception.StripeException import com.stripe.android.model.BankStatuses import com.stripe.android.model.CardMetadata import com.stripe.android.model.ConfirmPaymentIntentParams @@ -26,6 +27,7 @@ import com.stripe.android.model.ListPaymentMethodsParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.ShippingInformation import com.stripe.android.model.Source @@ -46,6 +48,7 @@ import com.stripe.android.model.parsers.ModelJsonParser import com.stripe.android.model.parsers.PaymentIntentJsonParser import com.stripe.android.model.parsers.PaymentMethodJsonParser import com.stripe.android.model.parsers.PaymentMethodsListJsonParser +import com.stripe.android.model.parsers.RadarSessionJsonParser import com.stripe.android.model.parsers.SetupIntentJsonParser import com.stripe.android.model.parsers.SourceJsonParser import com.stripe.android.model.parsers.Stripe3ds2AuthResultJsonParser @@ -77,7 +80,7 @@ internal class StripeApiRepository @JvmOverloads internal constructor( private val analyticsRequestExecutor: AnalyticsRequestExecutor = AnalyticsRequestExecutor.Default(logger), private val fingerprintDataRepository: FingerprintDataRepository = - FingerprintDataRepository.Default(context), + FingerprintDataRepository.Default(context, workContext), private val analyticsRequestFactory: AnalyticsRequestFactory = AnalyticsRequestFactory(context, publishableKey), private val fingerprintParamsUtils: FingerprintParamsUtils = FingerprintParamsUtils(), @@ -92,7 +95,7 @@ internal class StripeApiRepository @JvmOverloads internal constructor( ) private val fingerprintData: FingerprintData? - get() = fingerprintDataRepository.get() + get() = fingerprintDataRepository.getCached() init { fireFingerprintRequest() @@ -921,6 +924,43 @@ internal class StripeApiRepository @JvmOverloads internal constructor( return response.responseJson } + /** + * Get the latest [FingerprintData] from [FingerprintDataRepository] and send in POST request + * to `/v1/radar/session`. + */ + override suspend fun createRadarSession( + requestOptions: ApiRequest.Options + ): RadarSession? { + return runCatching { + require(Stripe.advancedFraudSignalsEnabled) { + "Stripe.advancedFraudSignalsEnabled must be set to 'true' to create a Radar Session." + } + requireNotNull(fingerprintDataRepository.getLatest()) { + "Could not obtain fraud data required to create a Radar Session." + } + }.map { + val params = it.params.plus( + mapOf( + "payment_user_agent" to "stripe-android/${Stripe.VERSION_NAME}" + ) + ) + fetchStripeModel( + apiRequestFactory.createPost( + getApiUrl("radar/session"), + requestOptions, + params + ), + RadarSessionJsonParser() + ) { + fireAnalyticsRequest( + analyticsRequestFactory.createRequest(AnalyticsEvent.RadarSessionCreate) + ) + } + }.getOrElse { + throw StripeException.create(it) + } + } + /** * @return `https://api.stripe.com/v1/payment_methods/:id/detach` */ diff --git a/stripe/src/main/java/com/stripe/android/networking/StripeRepository.kt b/stripe/src/main/java/com/stripe/android/networking/StripeRepository.kt index ddd8d498d10..fedb09061d7 100644 --- a/stripe/src/main/java/com/stripe/android/networking/StripeRepository.kt +++ b/stripe/src/main/java/com/stripe/android/networking/StripeRepository.kt @@ -15,6 +15,7 @@ import com.stripe.android.model.ListPaymentMethodsParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.ShippingInformation import com.stripe.android.model.Source @@ -322,4 +323,8 @@ internal interface StripeRepository { url: String, requestOptions: ApiRequest.Options ): JSONObject + + suspend fun createRadarSession( + requestOptions: ApiRequest.Options + ): RadarSession? } diff --git a/stripe/src/test/java/com/stripe/android/FakeFingerprintDataRepository.kt b/stripe/src/test/java/com/stripe/android/FakeFingerprintDataRepository.kt index 9d34e946688..2cff71f827a 100644 --- a/stripe/src/test/java/com/stripe/android/FakeFingerprintDataRepository.kt +++ b/stripe/src/test/java/com/stripe/android/FakeFingerprintDataRepository.kt @@ -3,21 +3,28 @@ package com.stripe.android import java.util.UUID internal class FakeFingerprintDataRepository( - private val guid: UUID = UUID.randomUUID(), - private val muid: UUID = UUID.randomUUID(), - private val sid: UUID = UUID.randomUUID() + private val fingerprintData: FingerprintData? ) : FingerprintDataRepository { - override fun refresh() { - } - override fun get(): FingerprintData? { - return FingerprintData( + @JvmOverloads constructor( + guid: UUID = UUID.randomUUID(), + muid: UUID = UUID.randomUUID(), + sid: UUID = UUID.randomUUID() + ) : this( + FingerprintData( guid = guid.toString(), muid = muid.toString(), sid = sid.toString() ) + ) + + override fun refresh() { } + override fun getCached() = fingerprintData + + override suspend fun getLatest() = fingerprintData + override fun save(fingerprintData: FingerprintData) { } } diff --git a/stripe/src/test/java/com/stripe/android/FingerprintDataRepositoryTest.kt b/stripe/src/test/java/com/stripe/android/FingerprintDataRepositoryTest.kt index d9f245c9921..662f2ddc25d 100644 --- a/stripe/src/test/java/com/stripe/android/FingerprintDataRepositoryTest.kt +++ b/stripe/src/test/java/com/stripe/android/FingerprintDataRepositoryTest.kt @@ -39,7 +39,7 @@ class FingerprintDataRepositoryTest { val repository = FingerprintDataRepository.Default(context) repository.save(expectedFingerprintData) repository.refresh() - assertThat(repository.get()) + assertThat(repository.getCached()) .isEqualTo(expectedFingerprintData) } @@ -59,7 +59,7 @@ class FingerprintDataRepositoryTest { ) repository.save(createFingerprintData(elapsedTime = -60L)) repository.refresh() - val actualFingerprintData = repository.get() + val actualFingerprintData = repository.getCached() assertThat(actualFingerprintData) .isEqualTo(expectedFingerprintData) diff --git a/stripe/src/test/java/com/stripe/android/StripeEndToEndTest.kt b/stripe/src/test/java/com/stripe/android/StripeEndToEndTest.kt index 211ceef6798..f4f40bb7b2e 100644 --- a/stripe/src/test/java/com/stripe/android/StripeEndToEndTest.kt +++ b/stripe/src/test/java/com/stripe/android/StripeEndToEndTest.kt @@ -218,6 +218,13 @@ internal class StripeEndToEndTest { ).isTrue() } + @Test + fun `createRadarSession() should return a valid Radar Session id`() = testDispatcher.runBlockingTest { + val radarSession = createStripeWithTestScope().createRadarSession() + assertThat(radarSession.id) + .startsWith("rse_") + } + private fun createStripeWithTestScope( publishableKey: String = ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY ): Stripe { diff --git a/stripe/src/test/java/com/stripe/android/model/parsers/RadarSessionJsonParserTest.kt b/stripe/src/test/java/com/stripe/android/model/parsers/RadarSessionJsonParserTest.kt new file mode 100644 index 00000000000..8cec7bd5727 --- /dev/null +++ b/stripe/src/test/java/com/stripe/android/model/parsers/RadarSessionJsonParserTest.kt @@ -0,0 +1,27 @@ +package com.stripe.android.model.parsers + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.model.RadarSession +import org.json.JSONObject +import kotlin.test.Test + +class RadarSessionJsonParserTest { + + @Test + fun `parse should return expected object`() { + assertThat(RadarSessionJsonParser().parse(JSON)) + .isEqualTo( + RadarSession( + id = "rse_abc123" + ) + ) + } + + private companion object { + private val JSON = JSONObject( + """ + {"id": "rse_abc123"} + """.trimIndent() + ) + } +} diff --git a/stripe/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt b/stripe/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt index e91f4df44a9..eb30926c829 100644 --- a/stripe/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt +++ b/stripe/src/test/java/com/stripe/android/networking/AbsFakeStripeRepository.kt @@ -12,6 +12,7 @@ import com.stripe.android.model.ListPaymentMethodsParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.ShippingInformation import com.stripe.android.model.Source @@ -236,4 +237,8 @@ internal abstract class AbsFakeStripeRepository : StripeRepository { url: String, requestOptions: ApiRequest.Options ) = JSONObject() + + override suspend fun createRadarSession( + requestOptions: ApiRequest.Options + ) = RadarSession("rse_abc123") } diff --git a/stripe/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt b/stripe/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt index dd36dd68c41..0b4b4491d47 100644 --- a/stripe/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt +++ b/stripe/src/test/java/com/stripe/android/networking/StripeApiRepositoryTest.kt @@ -12,11 +12,15 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import com.nhaarman.mockitokotlin2.verifyZeroInteractions import com.nhaarman.mockitokotlin2.whenever import com.stripe.android.ApiKeyFixtures +import com.stripe.android.FakeFingerprintDataRepository import com.stripe.android.FileFactory +import com.stripe.android.FingerprintData import com.stripe.android.FingerprintDataFixtures import com.stripe.android.FingerprintDataRepository +import com.stripe.android.Stripe import com.stripe.android.exception.APIConnectionException import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.BankAccountTokenParamsFixtures @@ -80,13 +84,14 @@ internal class StripeApiRepositoryTest { @BeforeTest fun before() { - whenever(fingerprintDataRepository.get()).thenReturn( + whenever(fingerprintDataRepository.getCached()).thenReturn( FingerprintDataFixtures.create(Calendar.getInstance().timeInMillis) ) } @AfterTest fun cleanup() { + Stripe.advancedFraudSignalsEnabled = true testDispatcher.cleanupTestCoroutines() } @@ -951,6 +956,61 @@ internal class StripeApiRepositoryTest { ) } + @Test + fun `createRadarSession() with FingerprintData should return expected value`() = testDispatcher.runBlockingTest { + val stripeRepository = StripeApiRepository( + context, + DEFAULT_OPTIONS.apiKey, + analyticsRequestExecutor = analyticsRequestExecutor, + fingerprintDataRepository = FakeFingerprintDataRepository( + FingerprintData( + guid = "8ae65368-76c5-4dd5-81b9-279f61efa591c80a51", + muid = "ac3febde-f658-41b5-8c4d-94905501c7a6f4ca3c", + sid = "02892cd4-183a-4074-bca2-5dc0647dd816ce4cbf" + ) + ), + workContext = testDispatcher + ) + val radarSession = requireNotNull( + stripeRepository.createRadarSession(DEFAULT_OPTIONS) + ) + assertThat(radarSession.id) + .startsWith("rse_") + + verifyAnalyticsRequest(AnalyticsEvent.RadarSessionCreate) + } + + @Test + fun `createRadarSession() with null FingerprintData should throw an exception`() = testDispatcher.runBlockingTest { + val stripeRepository = StripeApiRepository( + context, + DEFAULT_OPTIONS.apiKey, + fingerprintDataRepository = FakeFingerprintDataRepository( + null + ), + workContext = testDispatcher + ) + + val invalidRequestException = assertFailsWith { + stripeRepository.createRadarSession(DEFAULT_OPTIONS) + } + assertThat(invalidRequestException.message) + .isEqualTo("Could not obtain fraud data required to create a Radar Session.") + } + + @Test + fun `createRadarSession() with advancedFraudSignalsEnabled set to false should throw an exception`() = testDispatcher.runBlockingTest { + verifyZeroInteractions(fingerprintDataRepository) + + Stripe.advancedFraudSignalsEnabled = false + val stripeRepository = create() + val invalidRequestException = assertFailsWith { + stripeRepository.createRadarSession(DEFAULT_OPTIONS) + } + assertThat(invalidRequestException.message) + .isEqualTo("Stripe.advancedFraudSignalsEnabled must be set to 'true' to create a Radar Session.") + } + private fun verifyFingerprintAndAnalyticsRequests( event: AnalyticsEvent, productUsage: List? = null From 53aad7a9b25fae4b31c476f9c78f631ed515421a Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 19 May 2021 12:39:28 -0400 Subject: [PATCH 2/6] Update API --- stripe/api/stripe.api | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/stripe/api/stripe.api b/stripe/api/stripe.api index 3b06d6f9d87..55f6f00a802 100644 --- a/stripe/api/stripe.api +++ b/stripe/api/stripe.api @@ -931,6 +931,9 @@ public final class com/stripe/android/Stripe { public final fun createPiiTokenSynchronous (Ljava/lang/String;Ljava/lang/String;)Lcom/stripe/android/model/Token; public final fun createPiiTokenSynchronous (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/stripe/android/model/Token; public static synthetic fun createPiiTokenSynchronous$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/Token; + public final fun createRadarSession (Lcom/stripe/android/ApiResultCallback;)V + public final fun createRadarSession (Ljava/lang/String;Lcom/stripe/android/ApiResultCallback;)V + public static synthetic fun createRadarSession$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Lcom/stripe/android/ApiResultCallback;ILjava/lang/Object;)V public final fun createSource (Lcom/stripe/android/model/SourceParams;Lcom/stripe/android/ApiResultCallback;)V public final fun createSource (Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Lcom/stripe/android/ApiResultCallback;)V public final fun createSource (Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lcom/stripe/android/ApiResultCallback;)V @@ -1083,6 +1086,7 @@ public final class com/stripe/android/StripeKtxKt { public static synthetic fun createPersonToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/PersonTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun createPiiToken (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createPiiToken$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createRadarSession (Lcom/stripe/android/Stripe;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun createSource (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createSource$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun getAuthenticateSourceResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -4092,6 +4096,28 @@ public final class com/stripe/android/model/PiiTokenParams$Creator : android/os/ public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/model/RadarSession : com/stripe/android/model/StripeModel { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lcom/stripe/android/model/RadarSession; + public static synthetic fun copy$default (Lcom/stripe/android/model/RadarSession;Ljava/lang/String;ILjava/lang/Object;)Lcom/stripe/android/model/RadarSession; + public fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class com/stripe/android/model/RadarSession$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/model/RadarSession; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/model/RadarSession; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/model/SetupIntent : com/stripe/android/model/StripeIntent { public static final field CREATOR Landroid/os/Parcelable$Creator; public static final field Companion Lcom/stripe/android/model/SetupIntent$Companion; From d70c2b2451520d3f026a67b0ea993457eab5320b Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 19 May 2021 12:47:41 -0400 Subject: [PATCH 3/6] Fix test --- .../java/com/stripe/android/FingerprintDataRepository.kt | 6 ++++-- .../com/stripe/android/networking/StripeApiRepository.kt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt index a9a024b834a..0bbffa5a39d 100644 --- a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt +++ b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt @@ -37,8 +37,7 @@ internal interface FingerprintDataRepository { class Default( private val localStore: FingerprintDataStore, private val fingerprintRequestFactory: FingerprintRequestFactory, - private val fingerprintRequestExecutor: FingerprintRequestExecutor = - FingerprintRequestExecutor.Default(), + private val fingerprintRequestExecutor: FingerprintRequestExecutor, private val workContext: CoroutineContext ) : FingerprintDataRepository { private var cachedFingerprintData: FingerprintData? = null @@ -54,6 +53,9 @@ internal interface FingerprintDataRepository { ) : this( localStore = FingerprintDataStore.Default(context, workContext), fingerprintRequestFactory = FingerprintRequestFactory.Default(context), + fingerprintRequestExecutor = FingerprintRequestExecutor.Default( + workContext = workContext + ), workContext = workContext ) diff --git a/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt b/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt index 9b60b56686c..e6345273957 100644 --- a/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt +++ b/stripe/src/main/java/com/stripe/android/networking/StripeApiRepository.kt @@ -78,7 +78,7 @@ internal class StripeApiRepository @JvmOverloads internal constructor( logger = logger ), private val analyticsRequestExecutor: AnalyticsRequestExecutor = - AnalyticsRequestExecutor.Default(logger), + AnalyticsRequestExecutor.Default(logger, workContext), private val fingerprintDataRepository: FingerprintDataRepository = FingerprintDataRepository.Default(context, workContext), private val analyticsRequestFactory: AnalyticsRequestFactory = From aa2bc7d8dfd6d022811be57e04cb52ad5cf00f49 Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 19 May 2021 15:21:42 -0400 Subject: [PATCH 4/6] Update docs --- stripe/src/main/java/com/stripe/android/Stripe.kt | 4 +++- stripe/src/main/java/com/stripe/android/StripeKtx.kt | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/stripe/src/main/java/com/stripe/android/Stripe.kt b/stripe/src/main/java/com/stripe/android/Stripe.kt index 48258449f45..116ca7f7ad6 100644 --- a/stripe/src/main/java/com/stripe/android/Stripe.kt +++ b/stripe/src/main/java/com/stripe/android/Stripe.kt @@ -2008,7 +2008,9 @@ class Stripe internal constructor( } /** - * Create a Radar Session asynchronously + * Create a Radar Session asynchronously. + * + * [Stripe.advancedFraudSignalsEnabled] must be `true` to use this method. * * @param stripeAccountId Optional, the Connect account to associate with this request. * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index 6101a7dca9e..0275875d281 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -426,6 +426,8 @@ suspend fun Stripe.createFile( /** * Create a Radar Session. * + * [Stripe.advancedFraudSignalsEnabled] must be `true` to use this method. + * * @throws AuthenticationException failure to properly authenticate yourself (check your key) * @throws InvalidRequestException your request has invalid parameters * @throws APIConnectionException failure to connect to Stripe's API From 9208f2741acb76c7780a639414ef0eb0ccc5753d Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 19 May 2021 16:05:38 -0400 Subject: [PATCH 5/6] Update docs --- stripe/src/main/java/com/stripe/android/Stripe.kt | 2 ++ stripe/src/main/java/com/stripe/android/StripeKtx.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/stripe/src/main/java/com/stripe/android/Stripe.kt b/stripe/src/main/java/com/stripe/android/Stripe.kt index 116ca7f7ad6..6700c40a55e 100644 --- a/stripe/src/main/java/com/stripe/android/Stripe.kt +++ b/stripe/src/main/java/com/stripe/android/Stripe.kt @@ -2012,6 +2012,8 @@ class Stripe internal constructor( * * [Stripe.advancedFraudSignalsEnabled] must be `true` to use this method. * + * See the [Radar Session](https://stripe.com/docs/radar/radar-session) docs for more details. + * * @param stripeAccountId Optional, the Connect account to associate with this request. * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. * @param callback a [ApiResultCallback] to receive the result or error diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index 0275875d281..4adc18d41ca 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -428,6 +428,8 @@ suspend fun Stripe.createFile( * * [Stripe.advancedFraudSignalsEnabled] must be `true` to use this method. * + * See the [Radar Session](https://stripe.com/docs/radar/radar-session) docs for more details. + * * @throws AuthenticationException failure to properly authenticate yourself (check your key) * @throws InvalidRequestException your request has invalid parameters * @throws APIConnectionException failure to connect to Stripe's API From 80465e99cf0245915a15a4b618c324e079a122a4 Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 19 May 2021 19:58:36 -0400 Subject: [PATCH 6/6] Remote annotations --- .../java/com/stripe/android/FingerprintDataRepository.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt index 0bbffa5a39d..e610a65eb85 100644 --- a/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt +++ b/stripe/src/main/java/com/stripe/android/FingerprintDataRepository.kt @@ -1,8 +1,6 @@ package com.stripe.android import android.content.Context -import androidx.annotation.UiThread -import androidx.annotation.WorkerThread import com.stripe.android.networking.FingerprintRequestExecutor import com.stripe.android.networking.FingerprintRequestFactory import kotlinx.coroutines.CoroutineScope @@ -13,13 +11,11 @@ import java.util.Calendar import kotlin.coroutines.CoroutineContext internal interface FingerprintDataRepository { - @UiThread fun refresh() /** * Get the cached [FingerprintData]. This is not a blocking request. */ - @UiThread fun getCached(): FingerprintData? /** @@ -28,10 +24,8 @@ internal interface FingerprintDataRepository { * 1. From [FingerprintDataStore] if that value is not expired. * 2. Otherwise, from the network. */ - @WorkerThread suspend fun getLatest(): FingerprintData? - @UiThread fun save(fingerprintData: FingerprintData) class Default(