Skip to content

Commit

Permalink
Merge pull request #109 from Tinkoff/2.10.0
Browse files Browse the repository at this point in the history
2.10.0
  • Loading branch information
jQwout authored Nov 11, 2022
2 parents c214120 + 771b694 commit 4d8595f
Show file tree
Hide file tree
Showing 80 changed files with 1,380 additions and 516 deletions.
12 changes: 12 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 2.10.0

#### Fixed
#### Changes
- Deleted `CollectDataState`; 3DS data collection is handled internally now
- Changed `NetworkClient` to utilize okhttp
- Tinkoff Pay button redesign
- Changed card pan validation mechanism; added Union Pay system recognition
- Changed names of some view attributes ([migration](/migration.md))
- Add 3DS v2 flow for attach card
#### Additions

## 2.9.0

#### Fixed
Expand Down
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ repositories {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "com.squareup.okhttp3:okhttp:${okHttpVersion}"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation 'junit:junit:4.13'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ object AcquiringApi {

internal const val JSON = "application/json"
internal const val FORM_URL_ENCODED = "application/x-www-form-urlencoded"
internal const val TIMEOUT = 40000
internal const val TIMEOUT = 40000L

private const val API_URL_RELEASE_OLD = "https://securepay.tinkoff.ru/rest"
private const val API_URL_DEBUG_OLD = "https://rest-api-test.tcsbank.ru/rest"
Expand Down
133 changes: 40 additions & 93 deletions core/src/main/java/ru/tinkoff/acquiring/sdk/network/NetworkClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ package ru.tinkoff.acquiring.sdk.network
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParseException
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import ru.tinkoff.acquiring.sdk.AcquiringSdk
import ru.tinkoff.acquiring.sdk.exceptions.AcquiringApiException
import ru.tinkoff.acquiring.sdk.exceptions.NetworkException
import ru.tinkoff.acquiring.sdk.models.enums.CardStatus
import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus
import ru.tinkoff.acquiring.sdk.models.enums.Tax
import ru.tinkoff.acquiring.sdk.models.enums.Taxation
import ru.tinkoff.acquiring.sdk.network.AcquiringApi.FORM_URL_ENCODED
import ru.tinkoff.acquiring.sdk.network.AcquiringApi.JSON
import ru.tinkoff.acquiring.sdk.network.AcquiringApi.STREAM_BUFFER_SIZE
import ru.tinkoff.acquiring.sdk.network.AcquiringApi.TIMEOUT
import ru.tinkoff.acquiring.sdk.requests.AcquiringRequest
import ru.tinkoff.acquiring.sdk.requests.FinishAuthorizeRequest
Expand All @@ -37,61 +39,44 @@ import ru.tinkoff.acquiring.sdk.responses.GetCardListResponse
import ru.tinkoff.acquiring.sdk.utils.serialization.*
import java.io.*
import java.lang.reflect.Modifier
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_OK
import java.net.MalformedURLException
import java.net.URL
import java.net.URLEncoder
import java.util.concurrent.TimeUnit

/**
* @author Mariya Chernyadieva, Taras Nagorny
*/
internal class NetworkClient {

private val gson: Gson = createGson()
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.build()

internal fun <R : AcquiringResponse> call(request: AcquiringRequest<R>,
responseClass: Class<R>,
onSuccess: (R) -> Unit,
onFailure: (Exception) -> Unit) {
private val gson: Gson = createGson()

val result: R
internal fun <R : AcquiringResponse> call(
request: AcquiringRequest<R>,
responseClass: Class<R>,
onSuccess: (R) -> Unit,
onFailure: (Exception) -> Unit
) {
var response: String? = null
var responseReader: InputStreamReader? = null
var requestContentStream: OutputStream? = null

try {
lateinit var connection: HttpURLConnection
val httpRequest = request.httpRequest()
val call = okHttpClient.newCall(httpRequest)
val httpResponse = call.execute()

when (request.httpRequestMethod) {
AcquiringApi.API_REQUEST_METHOD_GET -> {
prepareConnection(request) {
connection = it

AcquiringSdk.log("=== Sending ${request.httpRequestMethod} request to ${connection.url}")
}
}
AcquiringApi.API_REQUEST_METHOD_POST -> {
prepareBody(request) { body ->
prepareConnection(request) {
connection = it
connection.setRequestProperty("Content-length", body.size.toString())
requestContentStream = connection.outputStream
requestContentStream?.write(body)

AcquiringSdk.log("=== Sending ${request.httpRequestMethod} request to ${connection.url}")
}
}
}
}
AcquiringSdk.log("=== Sending ${httpRequest.method} request to ${httpRequest.url}")

val responseCode = connection.responseCode
val responseCode = httpResponse.code
response = httpResponse.body?.string()

if (responseCode == HTTP_OK) {
responseReader = InputStreamReader(connection.inputStream)
response = read(responseReader)
AcquiringSdk.log("=== Got server response: $response")
result = gson.fromJson(response, responseClass)
val result = gson.fromJson(response, responseClass)

checkResult(result) { isSuccess ->
if (!request.isDisposed()) {
Expand All @@ -106,9 +91,7 @@ internal class NetworkClient {
}

} else {
responseReader = InputStreamReader(connection.errorStream)
response = read(responseReader)
if (response.isNotEmpty()) {
if (!response.isNullOrEmpty()) {
AcquiringSdk.log("=== Got server error response: $response")
} else {
AcquiringSdk.log("=== Got server error response code: $responseCode")
Expand All @@ -126,41 +109,30 @@ internal class NetworkClient {
if (!request.isDisposed()) {
onFailure(AcquiringApiException("Invalid response. $response", e))
}
} finally {
closeQuietly(responseReader)
closeQuietly(requestContentStream)
}
}

private fun <R : AcquiringResponse> prepareConnection(request: AcquiringRequest<R>, onReady: (HttpURLConnection) -> Unit) {
val targetUrl = prepareURL(request.apiMethod)
val connection = targetUrl.openConnection() as HttpURLConnection

with(connection) {
requestMethod = request.httpRequestMethod
connectTimeout = TIMEOUT
readTimeout = TIMEOUT
doOutput = when (request.httpRequestMethod) {
AcquiringApi.API_REQUEST_METHOD_GET -> false
else -> true
private fun AcquiringRequest<*>.httpRequest() = Request.Builder().also { builder ->
builder.url(prepareURL(apiMethod))
when (httpRequestMethod) {
AcquiringApi.API_REQUEST_METHOD_GET -> builder.get()
AcquiringApi.API_REQUEST_METHOD_POST -> {
val body = getRequestBody()
AcquiringSdk.log("=== Parameters: $body")
builder.post(body.toRequestBody(contentType.toMediaType()))
}
setRequestProperty("Content-type", request.contentType)
}

if (request is FinishAuthorizeRequest && request.is3DsVersionV2()) {
setRequestProperty("User-Agent", System.getProperty("http.agent"))
setRequestProperty("Accept", JSON)
}
if (this is FinishAuthorizeRequest && is3DsVersionV2()) {
builder.header("User-Agent", System.getProperty("http.agent"))
builder.header("Accept", JSON)
}

onReady(connection)
}
getHeaders().forEach { (key, value) ->
builder.header(key, value)
}

private fun <R : AcquiringResponse> prepareBody(request: AcquiringRequest<R>, onReady: (ByteArray) -> Unit) {
val requestBody = request.getRequestBody()
AcquiringSdk.log("=== Parameters: $requestBody")

onReady(requestBody.toByteArray())
}
}.build()

private fun <R : AcquiringResponse> checkResult(result: R, onChecked: (isSuccess: Boolean) -> Unit) {
if (result.errorCode == AcquiringApi.API_ERROR_CODE_NO_ERROR && result.isSuccess!!) {
Expand All @@ -185,31 +157,6 @@ internal class NetworkClient {
return URL(builder.toString())
}

@Throws(IOException::class)
private fun read(reader: InputStreamReader): String {
val buffer = CharArray(STREAM_BUFFER_SIZE)
var read: Int = -1
val result = StringBuilder()

while ({ read = reader.read(buffer, 0, STREAM_BUFFER_SIZE); read }() != -1) {
result.append(buffer, 0, read)
}

return result.toString()
}

private fun closeQuietly(closeable: Closeable?) {
if (closeable == null) {
return
}

try {
closeable.close()
} catch (e: IOException) {
AcquiringSdk.log(e)
}
}

companion object {

fun createGson(): Gson {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package ru.tinkoff.acquiring.sdk.requests
import com.google.gson.Gson
import ru.tinkoff.acquiring.sdk.AcquiringSdk
import ru.tinkoff.acquiring.sdk.network.AcquiringApi
import ru.tinkoff.acquiring.sdk.network.AcquiringApi.JSON
import ru.tinkoff.acquiring.sdk.network.NetworkClient
import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse
import ru.tinkoff.acquiring.sdk.utils.Request
Expand All @@ -43,6 +44,7 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
@Volatile
private var disposed = false
private val ignoredFieldsSet: HashSet<String> = hashSetOf(DATA, RECEIPT, RECEIPTS, SHOPS)
private val headersMap: HashMap<String, String> = hashMapOf()

internal open val tokenIgnoreFields: HashSet<String>
get() = ignoredFieldsSet
Expand All @@ -66,10 +68,12 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
return map
}

protected fun <R : AcquiringResponse> performRequest(request: AcquiringRequest<R>,
responseClass: Class<R>,
onSuccess: (R) -> Unit,
onFailure: (Exception) -> Unit) {
protected fun <R : AcquiringResponse> performRequest(
request: AcquiringRequest<R>,
responseClass: Class<R>,
onSuccess: (R) -> Unit,
onFailure: (Exception) -> Unit
) {
request.validate()
val client = NetworkClient()
client.call(request, responseClass, onSuccess, onFailure)
Expand Down Expand Up @@ -104,9 +108,19 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
}
}

fun addUserAgentHeader(userAgent: String = System.getProperty("http.agent")) {
headersMap.put("User-Agent", userAgent)
}

fun addContentHeader(content: String = JSON) {
headersMap.put("Accept", content)
}

protected open fun getToken(): String? =
AcquiringSdk.tokenGenerator?.generateToken(this, paramsForToken())

internal fun getHeaders() = headersMap

private fun paramsForToken(): MutableMap<String, Any> {
val tokenParams = asMap()
tokenIgnoreFields.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.google.gson.annotations.SerializedName
*
* @param customerKey идентификатор покупателя в системе продавца
* @param requestKey идентификатор запроса на привязку карты
* @param paymentId Уникальный идентификатор транзакции в системе Банка
* Возвращается, если в запросе был указан checkType != NO
*
* @author Mariya Chernyadieva
*/
Expand All @@ -31,6 +33,9 @@ class AddCardResponse(
val customerKey: String? = null,

@SerializedName("RequestKey")
val requestKey: String? = null
val requestKey: String? = null,

@SerializedName("PaymentId")
val paymentId: Long? = null

) : AcquiringResponse()
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ class AttachCardResponse(
val md: String? = null,

@SerializedName("PaReq")
val paReq: String? = null
val paReq: String? = null,

@SerializedName("AcsTransId")
val acsTransId: String? = null

) : AcquiringResponse() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ class Check3dsVersionResponse(
@SerializedName("PaymentSystem")
val paymentSystem: String? = null

) : AcquiringResponse()
) : AcquiringResponse() {

fun is3DsVersionV2() = version?.startsWith("2") ?: false
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import java.util.regex.Pattern
*/
internal object CardValidator {

private val allowedLengths = intArrayOf(13, 14, 15, 16, 17, 18, 19)
private val allowedLengths = 13..28
private const val ZERO_NUMBERS_CARD_NUMBER_REGEXP = "[0]{1,}"
private const val CVC_REGEXP = "^[0-9]{3}$"

Expand Down
Loading

0 comments on commit 4d8595f

Please sign in to comment.