diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt index 54fd9f1d6b6..2ba3cdb7ccf 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetSharedModule.kt @@ -8,11 +8,13 @@ import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID import com.stripe.android.core.networking.AnalyticsRequestExecutor import com.stripe.android.core.networking.AnalyticsRequestFactory import com.stripe.android.core.networking.AnalyticsRequestV2Executor +import com.stripe.android.core.networking.AnalyticsRequestV2Storage import com.stripe.android.core.networking.ApiRequest import com.stripe.android.core.networking.DefaultAnalyticsRequestExecutor import com.stripe.android.core.networking.DefaultAnalyticsRequestV2Executor import com.stripe.android.core.networking.DefaultStripeNetworkClient import com.stripe.android.core.networking.NetworkTypeDetector +import com.stripe.android.core.networking.RealAnalyticsRequestV2Storage import com.stripe.android.core.networking.StripeNetworkClient import com.stripe.android.core.utils.ContextUtils.packageInfo import com.stripe.android.core.utils.IsWorkManagerAvailable @@ -56,6 +58,10 @@ import kotlin.coroutines.CoroutineContext ) internal interface FinancialConnectionsSheetSharedModule { + @Binds + @Singleton + fun bindsAnalyticsRequestV2Storage(impl: RealAnalyticsRequestV2Storage): AnalyticsRequestV2Storage + @Binds @Singleton fun bindsAnalyticsRequestV2Executor(impl: DefaultAnalyticsRequestV2Executor): AnalyticsRequestV2Executor diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt index 57871469575..54cdb66bc20 100644 --- a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt +++ b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Executor.kt @@ -21,6 +21,7 @@ class DefaultAnalyticsRequestV2Executor @Inject constructor( private val application: Application, private val networkClient: StripeNetworkClient, private val logger: Logger, + private val storage: AnalyticsRequestV2Storage, private val isWorkManagerAvailable: IsWorkManagerAvailable, ) : AnalyticsRequestV2Executor { @@ -33,7 +34,8 @@ class DefaultAnalyticsRequestV2Executor @Inject constructor( private suspend fun enqueueRequest(request: AnalyticsRequestV2): Boolean { val workManager = WorkManager.getInstance(application) - val inputData = SendAnalyticsRequestV2Worker.createInputData(request) + val id = storage.store(request) + val inputData = SendAnalyticsRequestV2Worker.createInputData(id) val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Storage.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Storage.kt new file mode 100644 index 00000000000..d5836b5608e --- /dev/null +++ b/stripe-core/src/main/java/com/stripe/android/core/networking/AnalyticsRequestV2Storage.kt @@ -0,0 +1,59 @@ +package com.stripe.android.core.networking + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.RestrictTo +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.UUID +import javax.inject.Inject + +private const val AnalyticsRequestV2StorageName = "StripeAnalyticsRequestV2Storage" + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface AnalyticsRequestV2Storage { + suspend fun store(request: AnalyticsRequestV2): String + suspend fun retrieve(id: String): AnalyticsRequestV2? + suspend fun delete(id: String) +} + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class RealAnalyticsRequestV2Storage private constructor( + private val sharedPrefs: SharedPreferences, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +) : AnalyticsRequestV2Storage { + + @Inject constructor(application: Application) : this( + sharedPrefs = application.getSharedPreferences( + AnalyticsRequestV2StorageName, + Context.MODE_PRIVATE, + ), + ) + + override suspend fun store(request: AnalyticsRequestV2): String = withContext(dispatcher) { + val id = UUID.randomUUID().toString() + val encodedRequest = Json.encodeToString(request) + sharedPrefs + .edit() + .putString(id, encodedRequest) + .apply() + id + } + + override suspend fun retrieve(id: String): AnalyticsRequestV2? = withContext(dispatcher) { + val encodedRequest = sharedPrefs.getString(id, null) ?: return@withContext null + sharedPrefs.edit().remove(id).apply() + + runCatching { + Json.decodeFromString(encodedRequest) + }.getOrNull() + } + + override suspend fun delete(id: String) = withContext(dispatcher) { + sharedPrefs.edit().remove(id).apply() + } +} diff --git a/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt b/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt index c9436daa882..b31081a798d 100644 --- a/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt +++ b/stripe-core/src/main/java/com/stripe/android/core/networking/SendAnalyticsRequestV2Worker.kt @@ -1,5 +1,6 @@ package com.stripe.android.core.networking +import android.app.Application import android.content.Context import androidx.annotation.VisibleForTesting import androidx.work.CoroutineWorker @@ -7,8 +8,6 @@ import androidx.work.Data import androidx.work.WorkerParameters import androidx.work.workDataOf import com.stripe.android.core.exception.InvalidRequestException -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json private const val DataKey = "data" private const val MaxAttempts = 5 @@ -29,18 +28,25 @@ internal class SendAnalyticsRequestV2Worker( if (error.shouldRetry && runAttemptCount < MaxAttempts) { Result.retry() } else { + deleteRequest() Result.failure() } }, ) } - private inline fun withRequest(block: (AnalyticsRequestV2) -> Result): Result { - val request = getRequest(inputData) ?: return Result.failure() + private suspend inline fun withRequest(block: (AnalyticsRequestV2) -> Result): Result { + val id = inputData.getString(DataKey) ?: return Result.failure() + val request = storage(applicationContext).retrieve(id) ?: return Result.failure() val workManagerRequest = request.withWorkManagerParams(runAttemptCount) return block(workManagerRequest) } + private suspend fun deleteRequest() { + val id = inputData.getString(DataKey) ?: return + storage(applicationContext).delete(id) + } + companion object { const val TAG = "SendAnalyticsRequestV2Worker" @@ -48,24 +54,23 @@ internal class SendAnalyticsRequestV2Worker( var networkClient: StripeNetworkClient = DefaultStripeNetworkClient() private set - fun createInputData(request: AnalyticsRequestV2): Data { - val encodedRequest = Json.encodeToString(request) - return workDataOf(DataKey to encodedRequest) - } + var storage: (Context) -> AnalyticsRequestV2Storage = + { RealAnalyticsRequestV2Storage(it.applicationContext as Application) } + private set - private fun getRequest(data: Data): AnalyticsRequestV2? { - val encodedRequest = data.getString(DataKey) - return encodedRequest?.let { - runCatching { - Json.decodeFromString(it) - }.getOrNull() - } + fun createInputData(id: String): Data { + return workDataOf(DataKey to id) } @VisibleForTesting fun setNetworkClient(networkClient: StripeNetworkClient) { this.networkClient = networkClient } + + @VisibleForTesting + fun setStorage(storage: AnalyticsRequestV2Storage) { + this.storage = { storage } + } } } diff --git a/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt b/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt index 21f602d721c..dfad20798e9 100644 --- a/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt +++ b/stripe-core/src/test/java/com/stripe/android/core/networking/DefaultAnalyticsRequestV2ExecutorTest.kt @@ -7,6 +7,7 @@ import androidx.work.WorkManager import androidx.work.testing.WorkManagerTestInitHelper import com.google.common.truth.Truth.assertThat import com.stripe.android.core.Logger +import com.stripe.android.core.utils.FakeAnalyticsRequestV2Storage import com.stripe.android.core.utils.FakeStripeNetworkClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest @@ -29,11 +30,13 @@ internal class DefaultAnalyticsRequestV2ExecutorTest { @Test fun `Enqueues requests directly if WorkManager is available`() = runTest { val networkClient = FakeStripeNetworkClient() + val storage = FakeAnalyticsRequestV2Storage() val executor = DefaultAnalyticsRequestV2Executor( application = application, networkClient = networkClient, logger = Logger.noop(), + storage = storage, isWorkManagerAvailable = { true }, ) @@ -49,11 +52,13 @@ internal class DefaultAnalyticsRequestV2ExecutorTest { @Test fun `Executes requests directly if WorkManager isn't available`() = runTest { val networkClient = FakeStripeNetworkClient() + val storage = FakeAnalyticsRequestV2Storage() val executor = DefaultAnalyticsRequestV2Executor( application = application, networkClient = networkClient, logger = Logger.noop(), + storage = storage, isWorkManagerAvailable = { false }, ) diff --git a/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt b/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt index 574a1c8b869..dbd32233f19 100644 --- a/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt +++ b/stripe-core/src/test/java/com/stripe/android/core/networking/SendAnalyticsRequestV2WorkerTest.kt @@ -7,6 +7,7 @@ import androidx.work.testing.TestListenableWorkerBuilder import com.google.common.truth.Truth.assertThat import com.stripe.android.core.exception.APIConnectionException import com.stripe.android.core.exception.InvalidRequestException +import com.stripe.android.core.utils.FakeAnalyticsRequestV2Storage import com.stripe.android.core.utils.FakeStripeNetworkClient import kotlinx.coroutines.test.runTest import org.junit.Test @@ -46,17 +47,20 @@ internal class SendAnalyticsRequestV2WorkerTest { executeRequest: () -> StripeResponse, expectedResult: ListenableWorker.Result, ) { + val networkClient = FakeStripeNetworkClient(executeRequest = executeRequest) + SendAnalyticsRequestV2Worker.setNetworkClient(networkClient) + + val storage = FakeAnalyticsRequestV2Storage() + SendAnalyticsRequestV2Worker.setStorage(storage) + val request = mockAnalyticsRequest() - val input = SendAnalyticsRequestV2Worker.createInputData(request) + val id = storage.store(request) + val input = SendAnalyticsRequestV2Worker.createInputData(id) val worker = TestListenableWorkerBuilder(application) .setInputData(input) .build() - val networkClient = FakeStripeNetworkClient(executeRequest = executeRequest) - - SendAnalyticsRequestV2Worker.setNetworkClient(networkClient) - val result = worker.doWork() assertThat(result).isEqualTo(expectedResult) } diff --git a/stripe-core/src/test/java/com/stripe/android/core/utils/FakeAnalyticsRequestV2Storage.kt b/stripe-core/src/test/java/com/stripe/android/core/utils/FakeAnalyticsRequestV2Storage.kt new file mode 100644 index 00000000000..c00fc28f13d --- /dev/null +++ b/stripe-core/src/test/java/com/stripe/android/core/utils/FakeAnalyticsRequestV2Storage.kt @@ -0,0 +1,24 @@ +package com.stripe.android.core.utils + +import com.stripe.android.core.networking.AnalyticsRequestV2 +import com.stripe.android.core.networking.AnalyticsRequestV2Storage +import java.util.UUID + +internal class FakeAnalyticsRequestV2Storage : AnalyticsRequestV2Storage { + + private val store = mutableMapOf() + + override suspend fun store(request: AnalyticsRequestV2): String { + val id = UUID.randomUUID().toString() + store[id] = request + return id + } + + override suspend fun retrieve(id: String): AnalyticsRequestV2? { + return store[id] + } + + override suspend fun delete(id: String) { + store.remove(id) + } +}