diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt deleted file mode 100644 index b9fe7a24b..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joinforage.forage.android.core.services.vault - -import com.joinforage.forage.android.core.services.EnvConfig -import com.joinforage.forage.android.core.ui.element.ForageConfig - -internal object StopgapGlobalState { - var forageConfig: ForageConfig? = null - val envConfig: EnvConfig - get() = EnvConfig.fromForageConfig(forageConfig) -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt index 018305341..7ea1a562d 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt @@ -3,7 +3,7 @@ package com.joinforage.forage.android.core.ui.element import android.content.Context import android.util.AttributeSet import android.widget.LinearLayout -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.ui.element.state.ElementState /** @@ -34,11 +34,6 @@ abstract class AbstractForageElement( // update the forage config this._forageConfig = forageConfig - // TODO: 9/20/23: This is a temporary workaround and is - // not meant to stick around. See this doc for more details - // https://www.notion.so/joinforage/226d8ee6f8294d2694b1bb451791960b - StopgapGlobalState.forageConfig = forageConfig - // there are a number of side effect operations that we // need to run as soon as a ForageElement has access to // ForageConfig data. However, we don't want to run these @@ -60,4 +55,5 @@ abstract class AbstractForageElement( internal fun getForageConfig(): ForageConfig? { return _forageConfig } + internal fun getEnvConfig(): EnvConfig = EnvConfig.fromForageConfig(_forageConfig) } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt index d78a05ab8..20164b329 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt @@ -3,6 +3,7 @@ package com.joinforage.forage.android.ecom.services.vault.bt import com.basistheory.android.service.BasisTheoryElements import com.basistheory.android.service.ProxyRequest import com.basistheory.android.view.TextElement +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.ForageConstants import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys @@ -10,7 +11,6 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState import com.joinforage.forage.android.core.services.vault.VaultProxyRequest import com.joinforage.forage.android.core.services.vault.VaultSubmitterParams import com.joinforage.forage.android.core.ui.element.ForagePinElement @@ -19,8 +19,8 @@ internal typealias BasisTheoryResponse = Result internal class BasisTheoryPinSubmitter( foragePinEditText: ForagePinElement, + private val envConfig: EnvConfig, logger: Log, - private val buildVaultProvider: () -> BasisTheoryElements = { buildBt() } ) : AbstractVaultSubmitter( foragePinEditText = foragePinEditText, logger = logger @@ -40,11 +40,13 @@ internal class BasisTheoryPinSubmitter( encryptionKey = encryptionKey, vaultToken = vaultToken ) - .setHeader(ForageConstants.Headers.BT_PROXY_KEY, PROXY_ID) + .setHeader(ForageConstants.Headers.BT_PROXY_KEY, envConfig.btProxyID) .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") override suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse { - val bt = buildVaultProvider() + val bt = BasisTheoryElements.builder() + .apiKey(envConfig.btAPIKey) + .build() val proxyRequest: ProxyRequest = ProxyRequest().apply { headers = vaultProxyRequest.headers @@ -64,17 +66,4 @@ internal class BasisTheoryPinSubmitter( override fun getVaultToken(paymentMethod: PaymentMethod): String? = pickVaultTokenByIndex(paymentMethod, 1) - companion object { - // this code assumes that .setForageConfig() has been called - // on a Forage***EditText before PROXY_ID or API_KEY get - // referenced - private val PROXY_ID = StopgapGlobalState.envConfig.btProxyID - private val API_KEY = StopgapGlobalState.envConfig.btAPIKey - - private fun buildBt(): BasisTheoryElements { - return BasisTheoryElements.builder() - .apiKey(API_KEY) - .build() - } - } } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt index f345c9793..e94df5d24 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt @@ -1,8 +1,8 @@ package com.joinforage.forage.android.ecom.services.vault.vgs -import android.content.Context import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.forageapi.network.ForageApiError import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.network.ForageError @@ -10,7 +10,6 @@ import com.joinforage.forage.android.core.services.forageapi.network.UnknownErro import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState import com.joinforage.forage.android.core.services.vault.VaultProxyRequest import com.joinforage.forage.android.core.ui.element.ForagePinElement import com.verygoodsecurity.vgscollect.VGSCollectLogger @@ -25,8 +24,8 @@ import kotlin.coroutines.suspendCoroutine internal class VgsPinSubmitter( foragePinEditText: ForagePinElement, + private val envConfig: EnvConfig, logger: Log, - private val buildVaultProvider: (context: Context) -> VGSCollect = { buildVGSCollect(foragePinEditText.context) } ) : AbstractVaultSubmitter( foragePinEditText = foragePinEditText, logger = logger @@ -35,7 +34,11 @@ internal class VgsPinSubmitter( override suspend fun submitProxyRequest( vaultProxyRequest: VaultProxyRequest ): ForageApiResponse = suspendCoroutine { continuation -> - val vgsCollect = buildVaultProvider(foragePinEditText.context) + VGSCollectLogger.isEnabled = false + val vgsCollect = VGSCollect + .Builder(foragePinEditText.context, envConfig.vgsVaultId) + .setEnvironment(envConfig.vgsVaultType) + .create() vgsCollect.bindView(foragePinEditText.getTextElement() as VGSEditText) vgsCollect.addOnResponseListeners(object : VgsCollectResponseListener { @@ -66,20 +69,6 @@ internal class VgsPinSubmitter( override fun getVaultToken(paymentMethod: PaymentMethod): String? = pickVaultTokenByIndex(paymentMethod, 0) - companion object { - // this code assumes that .setForageConfig() has been called - // on a Forage***EditText before PROXY_ID or API_KEY get - // referenced - private val VAULT_ID = StopgapGlobalState.envConfig.vgsVaultId - private val VGS_ENVIRONMENT = StopgapGlobalState.envConfig.vgsVaultType - - private fun buildVGSCollect(context: Context): VGSCollect { - VGSCollectLogger.isEnabled = false - return VGSCollect.Builder(context, VAULT_ID) - .setEnvironment(VGS_ENVIRONMENT) - .create() - } - } fun toVaultErrorOrNull(vaultResponse: VGSResponse?): ForageApiResponse.Failure? { if (vaultResponse == null) return null diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt index 2b5aef093..8b0b86796 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt @@ -116,6 +116,7 @@ internal class BTVaultWrapper @JvmOverloads constructor( logger: Log ): AbstractVaultSubmitter = BasisTheoryPinSubmitter( foragePinElement, + foragePinElement.getEnvConfig(), logger ) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt index f0d19da75..6d53555eb 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt @@ -136,6 +136,7 @@ internal class VGSVaultWrapper @JvmOverloads constructor( logger: Log ): AbstractVaultSubmitter = VgsPinSubmitter( foragePinElement, + foragePinElement.getEnvConfig(), logger ) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt deleted file mode 100644 index 67defec09..000000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.joinforage.forage.android.vault - -import android.content.Context -import com.basistheory.android.service.BasisTheoryElements -import com.basistheory.android.service.ProxyApi -import com.basistheory.android.service.ProxyRequest -import com.basistheory.android.view.TextElement -import com.joinforage.forage.android.core.services.ForageConstants -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState -import com.joinforage.forage.android.core.services.vault.VaultProxyRequest -import com.joinforage.forage.android.core.ui.element.ForageConfig -import com.joinforage.forage.android.ecom.services.vault.bt.BasisTheoryPinSubmitter -import com.joinforage.forage.android.ecom.services.vault.bt.BasisTheoryResponse -import com.joinforage.forage.android.ecom.services.vault.bt.ProxyRequestObject -import com.joinforage.forage.android.ecom.ui.ForagePINEditText -import com.joinforage.forage.android.mock.MockLogger -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import me.jorgecastillo.hiroaki.internal.MockServerSuite -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq - -class BasisTheoryPinSubmitterTest() : MockServerSuite() { - private lateinit var mockLogger: MockLogger - private lateinit var mockForagePinEditText: ForagePINEditText - private lateinit var submitter: BasisTheoryPinSubmitter - private lateinit var mockBasisTheory: BasisTheoryElements - private lateinit var mockBasisTheoryTextElement: TextElement - private lateinit var mockApiProxy: ProxyApi - - @Before - fun setUp() { - super.setup() - - mockLogger = MockLogger() - - // Use Mockito judiciously (mainly for mocking views)! - // Opt for dependency injection and inheritance over Mockito - mockForagePinEditText = mock(ForagePINEditText::class.java) - val mockContext = mock(Context::class.java) - mockBasisTheory = mock(BasisTheoryElements::class.java) - mockBasisTheoryTextElement = mock(TextElement::class.java) - `when`(mockForagePinEditText.getTextElement()).thenReturn(mockBasisTheoryTextElement) - - // ensure we don't make any live requests! - mockBasisTheoryResponse(Result.success("success")) - - submitter = BasisTheoryPinSubmitter( - foragePinEditText = mockForagePinEditText, - logger = mockLogger, - buildVaultProvider = { mockBasisTheory } - ) - } - - private fun mockBasisTheoryResponse(response: BasisTheoryResponse) { - mockApiProxy = mock(ProxyApi::class.java) - `when`(mockBasisTheory.proxy).thenReturn(mockApiProxy) - runBlocking { - if (response.isSuccess) { - `when`(mockApiProxy.post(anyOrNull(), anyOrNull())).thenReturn(response.getOrNull()!!) - } else { - `when`(mockApiProxy.post(anyOrNull(), anyOrNull())).thenThrow(response.exceptionOrNull()!!) - } - } - } - - @Test - fun `Basis Theory request receives expected params`() = runTest { - StopgapGlobalState.forageConfig = ForageConfig("1234567", "sandbox_abcdefg123") - - val vaultProxyRequest = VaultProxyRequest.emptyRequest() - .setHeader(ForageConstants.Headers.X_KEY, "12320ce0-1a3c-4c64-970c-51ed7db34548") - .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, "1234567") - .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, "abcdef123") - .setHeader(ForageConstants.Headers.TRACE_ID, "65639248-03f2-498d-8aa8-9ebd1c60ee65") - .setToken("45320ce0-1a3c-4c64-970c-51ed7db34548") - .setPath("/api/payment_methods/defghij123/balance/") - .setHeader(ForageConstants.Headers.BT_PROXY_KEY, StopgapGlobalState.envConfig.btProxyID) - .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") - - runBlocking { - submitter.submitProxyRequest(vaultProxyRequest) - } - - val captor = argumentCaptor() - verify(mockApiProxy).post(captor.capture(), eq(null)) - val capturedRequest = captor.firstValue - - assertEquals( - ProxyRequestObject( - pin = mockBasisTheoryTextElement, - card_number_token = "45320ce0-1a3c-4c64-970c-51ed7db34548" - ), - capturedRequest.body - ) - assertEquals( - hashMapOf( - "X-KEY" to "12320ce0-1a3c-4c64-970c-51ed7db34548", - "Merchant-Account" to "1234567", - "IDEMPOTENCY-KEY" to "abcdef123", - "x-datadog-trace-id" to "65639248-03f2-498d-8aa8-9ebd1c60ee65", - "BT-PROXY-KEY" to "R1CNiogSdhnHeNq6ZFWrG1", // sandbox value of btProxyID - "Content-Type" to "application/json" - ), - capturedRequest.headers - ) - assertEquals("/api/payment_methods/defghij123/balance/", capturedRequest.path) - } - - @Test - fun `submitProxyRequest with valid input should return success`() = runTest { - val responseStr = """{"content_id":"32489e7e-13d9-499c-b017-f68a0122da95","message_type":"0200","status":"sent_to_proxy","failed":false,"errors":[]}""" - mockBasisTheoryResponse(Result.success(responseStr)) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Success) - assertEquals("[basis_theory] Received successful response from basis_theory", mockLogger.infoLogs.last().getMessage()) - assertEquals(responseStr, (result as ForageApiResponse.Success).data) - } - - @Test - fun `Basis Theory returns a vault error`() = runTest { - val basisTheoryErrorMessage = """ - Message: - HTTP response code: 400 - HTTP response body: {"proxy_error":{"errors":{"error":["Basis Theory Validation Error"]},"title":"One or more validation errors occurred.","status":400,"detail":"Bad Request"}} - HTTP response headers: ... - """.trimIndent() - mockBasisTheoryResponse(Result.failure(RuntimeException(basisTheoryErrorMessage))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals("[basis_theory] Received error from basis_theory: java.lang.RuntimeException: $basisTheoryErrorMessage", mockLogger.errorLogs.last().getMessage()) - assertEquals("Unknown Server Error", firstError.message) - assertEquals(500, firstError.httpStatusCode) - assertEquals("unknown_server_error", firstError.code) - } - - @Test - fun `Basis Theory returns a ForageError`() = runTest { - val responseStr = """ - Message: - HTTP response code: 400 - HTTP response body: {"path": "/api/payments/abcdefg123/collect_pin/","errors": [{"code": "too_many_requests","message": "Request was throttled, please try again later."}]} - HTTP response headers: {"some": "header"} - """.trimIndent() - - mockBasisTheoryResponse(Result.failure(RuntimeException(responseStr))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals( - """ - [basis_theory] Received ForageError from basis_theory: Code: too_many_requests - Message: Request was throttled, please try again later. - Status Code: 400 - Error Details (below): - null - """.trimIndent(), - mockLogger.errorLogs.last().getMessage() - ) - assertEquals("Request was throttled, please try again later.", firstError.message) - assertEquals(400, firstError.httpStatusCode) - assertEquals("too_many_requests", firstError.code) - } - - @Test - fun `Basis Theory responds with a malformed error`() = runTest { - val responseStr = """ - Malformed error! - """.trimIndent() - - mockBasisTheoryResponse(Result.failure(RuntimeException(responseStr))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals( - "[basis_theory] Received malformed response from basis_theory: Failure(java.lang.RuntimeException: Malformed error!)", - mockLogger.errorLogs.last().getMessage() - ) - assertEquals("Unknown Server Error", firstError.message) - assertEquals(500, firstError.httpStatusCode) - assertEquals("unknown_server_error", firstError.code) - } -}