diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b694c99..37da2cd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,17 +6,17 @@ ## Testing required outside of automated testing? -- [ ] Not Applicable +- [x] Not Applicable ### Screenshots (if appropriate): -- [ ] Not Applicable +- [x] Not Applicable ## Rollback / Rollforward Procedure -- [ ] Roll Forward +- [x] Roll Forward - [ ] Roll Back ## Reviewer Checklist diff --git a/example/src/main/java/com/basistheory/elements/example/util/TokenExpiration.kt b/example/src/main/java/com/basistheory/elements/example/util/TokenExpiration.kt index ffb0a24..f995faf 100644 --- a/example/src/main/java/com/basistheory/elements/example/util/TokenExpiration.kt +++ b/example/src/main/java/com/basistheory/elements/example/util/TokenExpiration.kt @@ -4,5 +4,5 @@ import org.threeten.bp.Instant import org.threeten.bp.temporal.ChronoUnit fun tokenExpirationTimestamp() = Instant.now() - .plus(5, ChronoUnit.MINUTES) + .plus(30, ChronoUnit.MINUTES) .toString() \ No newline at end of file diff --git a/example/src/main/java/com/basistheory/elements/example/view/card/CardFragment.kt b/example/src/main/java/com/basistheory/elements/example/view/card/CardFragment.kt index 59af988..118f6bf 100644 --- a/example/src/main/java/com/basistheory/elements/example/view/card/CardFragment.kt +++ b/example/src/main/java/com/basistheory/elements/example/view/card/CardFragment.kt @@ -1,7 +1,6 @@ package com.basistheory.elements.example.view.card import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,7 +10,6 @@ import com.basistheory.elements.constants.CoBadgedSupport import com.basistheory.elements.example.databinding.FragmentCardBinding import com.basistheory.elements.example.util.tokenExpirationTimestamp import com.basistheory.elements.example.viewmodel.CardFragmentViewModel -import com.basistheory.elements.service.BasisTheoryElements import com.basistheory.elements.view.CardBrandSelectorOptions class CardFragment : Fragment() { diff --git a/lib/src/main/java/com/basistheory/elements/service/ApiClientProvider.kt b/lib/src/main/java/com/basistheory/elements/service/ApiClientProvider.kt index fd252ad..a395d18 100644 --- a/lib/src/main/java/com/basistheory/elements/service/ApiClientProvider.kt +++ b/lib/src/main/java/com/basistheory/elements/service/ApiClientProvider.kt @@ -1,6 +1,7 @@ package com.basistheory.elements.service import com.basistheory.BasisTheoryApiClient +import com.basistheory.core.Environment import com.basistheory.resources.enrichments.EnrichmentsClient import com.basistheory.resources.sessions.SessionsClient import com.basistheory.resources.tokenintents.TokenIntentsClient @@ -10,7 +11,8 @@ import kotlinx.coroutines.Dispatchers internal class ApiClientProvider( private val apiUrl: String = "https://api.basistheory.com", - private val defaultApiKey: String? = null + private val defaultApiKey: String? = null, + private val environment: Environment? = Environment.DEFAULT ) { fun getTokensApi(apiKeyOverride: String? = null): TokensClient = getApiClient(apiKeyOverride).tokens() @@ -34,10 +36,15 @@ internal class ApiClientProvider( val apiKey = apiKeyOverride ?: defaultApiKey requireNotNull(apiKey) - return BasisTheoryApiClient.builder() + val apiClient = BasisTheoryApiClient.builder() .apiKey(apiKey) - .url(apiUrl) - .httpClient(createHttpClientWithDeviceInfo()) - .build() + .environment(environment) + .httpClient(createHttpClientWithDeviceInfo()); + + if (apiUrl != "https://api.basistheory.com" && environment === Environment.DEFAULT) { + apiClient.url(apiUrl) + } + + return apiClient.build() } } \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/elements/service/BasisTheoryElementsBuilder.kt b/lib/src/main/java/com/basistheory/elements/service/BasisTheoryElementsBuilder.kt index 409a5e5..3c54ad0 100644 --- a/lib/src/main/java/com/basistheory/elements/service/BasisTheoryElementsBuilder.kt +++ b/lib/src/main/java/com/basistheory/elements/service/BasisTheoryElementsBuilder.kt @@ -1,11 +1,13 @@ package com.basistheory.elements.service +import com.basistheory.core.Environment import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers class BasisTheoryElementsBuilder { private var _apiKey: String? = null private var _apiUrl: String = "https://api.basistheory.com" + private var _environment: Environment = Environment.DEFAULT private var _dispatcher: CoroutineDispatcher = Dispatchers.IO fun apiKey(value: String): BasisTheoryElementsBuilder { @@ -18,6 +20,11 @@ class BasisTheoryElementsBuilder { return this } + fun environment(value: Environment): BasisTheoryElementsBuilder { + _environment = value + return this + } + fun dispatcher(value: CoroutineDispatcher): BasisTheoryElementsBuilder { _dispatcher = value return this @@ -25,7 +32,7 @@ class BasisTheoryElementsBuilder { fun build(): BasisTheoryElements = BasisTheoryElements( - ApiClientProvider(_apiUrl, _apiKey), + ApiClientProvider(_apiUrl, _apiKey, _environment), _dispatcher ) } \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/elements/service/ProxyApi.kt b/lib/src/main/java/com/basistheory/elements/service/ProxyApi.kt index c17a004..c68a319 100644 --- a/lib/src/main/java/com/basistheory/elements/service/ProxyApi.kt +++ b/lib/src/main/java/com/basistheory/elements/service/ProxyApi.kt @@ -1,7 +1,8 @@ package com.basistheory.elements.service +import com.basistheory.core.Environment import com.basistheory.elements.model.ElementValueReference -import com.basistheory.elements.model.exceptions.ApiException +import com.basistheory.elements.util.getApiUrl import com.basistheory.elements.util.getEncodedDeviceInfo import com.basistheory.elements.util.isPrimitiveType import com.basistheory.elements.util.replaceElementRefs @@ -19,7 +20,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody - interface Proxy { suspend fun get(proxyRequest: ProxyRequest, apiKeyOverride: String? = null): Any? @@ -54,7 +54,8 @@ class ProxyApi( val dispatcher: CoroutineDispatcher = Dispatchers.IO, val apiBaseUrl: String = "https://api.basistheory.com", val apiKey: String, - val httpClient: OkHttpClient = OkHttpClient() + val httpClient: OkHttpClient = OkHttpClient(), + val environment: Environment = Environment.DEFAULT ) : Proxy { override suspend fun get(proxyRequest: ProxyRequest, apiKeyOverride: String?): Any? = @@ -94,7 +95,8 @@ class ProxyApi( } } - val urlBuilder = (apiBaseUrl + "/proxy" + (proxyRequest.path.orEmpty())) + val finalApiBaseUrl = if (apiBaseUrl !== "https://api.basistheory.com") apiBaseUrl else environment.getApiUrl() + val urlBuilder = (finalApiBaseUrl + "/proxy" + (proxyRequest.path.orEmpty())) .toHttpUrlOrNull()?.newBuilder() ?: throw IllegalArgumentException("Invalid URL") proxyRequest.queryParams?.toPairs()?.forEach { (key, value) -> @@ -112,6 +114,7 @@ class ProxyApi( val requestBody = when { method.equals("GET", ignoreCase = true) || method.equals("DELETE", ignoreCase = true) -> null + isTextPlain -> processedBody?.toString().orEmpty() .toRequestBody("text/plain; charset=utf-8".toMediaTypeOrNull()) diff --git a/lib/src/main/java/com/basistheory/elements/util/EnvironmentUtils.kt b/lib/src/main/java/com/basistheory/elements/util/EnvironmentUtils.kt new file mode 100644 index 0000000..662e46b --- /dev/null +++ b/lib/src/main/java/com/basistheory/elements/util/EnvironmentUtils.kt @@ -0,0 +1,15 @@ +package com.basistheory.elements.util + +import com.basistheory.core.Environment + +const val ApiUrl: String = "https://api.basistheory.com" +const val ApiTestUrl: String = "https://api.test.basistheory.com" + +fun Environment.getApiUrl(): String = + when (this) { + Environment.US -> ApiUrl + Environment.EU -> ApiUrl + Environment.TEST -> ApiTestUrl + Environment.DEFAULT -> ApiUrl + else -> ApiUrl + } \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/elements/view/CardNumberElement.kt b/lib/src/main/java/com/basistheory/elements/view/CardNumberElement.kt index b4d1949..467dfec 100644 --- a/lib/src/main/java/com/basistheory/elements/view/CardNumberElement.kt +++ b/lib/src/main/java/com/basistheory/elements/view/CardNumberElement.kt @@ -15,18 +15,16 @@ import com.basistheory.elements.model.BinDetails import com.basistheory.elements.model.BinRange import com.basistheory.elements.model.CardMetadata import com.basistheory.elements.model.InputType -import com.basistheory.elements.service.ApiClientProvider import com.basistheory.elements.service.CardBrandEnricher import com.basistheory.elements.util.BinDetailsCache import com.basistheory.elements.view.mask.ElementMask import com.basistheory.elements.view.transform.RegexReplaceElementTransform import com.basistheory.elements.view.validation.LuhnValidator -import com.basistheory.resources.enrichments.requests.EnrichmentsGetCardDetailsRequest -import com.basistheory.types.CardDetailsResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException class CardNumberElement @JvmOverloads constructor( context: Context, @@ -141,6 +139,8 @@ class CardNumberElement @JvmOverloads constructor( BinDetailsCache.put(bin, binDetails) updateBinDetails(binDetails) } + } catch (e: CancellationException) { + Log.i("CardNumberElement", "Fetch BIN details job cancelled") } catch (e: Exception) { Log.e("CardNumberElement", "Error fetching BIN details", e) } diff --git a/lib/src/test/java/com/basistheory/elements/service/ProxyApiTests.kt b/lib/src/test/java/com/basistheory/elements/service/ProxyApiTests.kt index 0682176..bd76715 100644 --- a/lib/src/test/java/com/basistheory/elements/service/ProxyApiTests.kt +++ b/lib/src/test/java/com/basistheory/elements/service/ProxyApiTests.kt @@ -1,5 +1,6 @@ package com.basistheory.elements.service +import com.basistheory.core.Environment import com.basistheory.elements.model.ElementValueReference import com.basistheory.elements.util.getEncodedDeviceInfo import io.mockk.CapturingSlot @@ -76,9 +77,9 @@ class ProxyApiTests { fun setup() { proxyApi = ProxyApi( dispatcher = Dispatchers.IO, - apiBaseUrl = "https://api.basistheory.com", apiKey = "124", - httpClient = mockHttpClient + httpClient = mockHttpClient, + environment = Environment.US ) proxyRequest = ProxyRequest() @@ -140,6 +141,43 @@ class ProxyApiTests { expectThat(result).isA() } + @Test + fun `should use uat url when environment is test`() { + proxyApi = ProxyApi( + dispatcher = Dispatchers.IO, + apiKey = "124", + httpClient = mockHttpClient, + environment = Environment.TEST + ) + + val queryParamValue = UUID.randomUUID().toString() + proxyRequest = proxyRequest.apply { + path = "/payment" + headers = mapOf( + "BT-PROXY-URL" to "https://echo.basistheory.com/post", + "Content-Type" to "text/plain" + ) + queryParams = mapOf("param" to queryParamValue) + body = "Hello World" + } + + val requestSlot = setupMocks("\"Hello World\"") + + val result = runBlocking { + proxyApi.post(proxyRequest) + } + + verify(exactly = 1) { mockHttpClient.newCall(any()) } + verify(exactly = 1) { mockCall.execute() } + + expectThat(requestSlot.captured) { + get { url.toString() } + .isEqualTo("https://api.test.basistheory.com/proxy/payment?param=${queryParamValue}") + } + + expectThat(result).isA() + } + @Test fun `should transform complex proxy response to element value references`() { val responseJson = """