Skip to content

Commit

Permalink
Merge pull request #101 from Tinkoff/2.6.0
Browse files Browse the repository at this point in the history
2.6.0 Add 3DS app-based flow via 3DS SDK
  • Loading branch information
IlnarH authored Aug 19, 2022
2 parents 8221483 + b3a0724 commit 1dfa379
Show file tree
Hide file tree
Showing 64 changed files with 1,175 additions and 363 deletions.
5 changes: 2 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ allprojects {
repositories {
jcenter()
google()
maven {
url "https://maven.google.com"
}
mavenCentral()
maven { url "https://maven.google.com" }
tasks.withType(Javadoc) {
options.addStringOption('encoding', 'UTF-8')
}
Expand Down
10 changes: 5 additions & 5 deletions cardio/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: '../gradle/dokka.gradle'
apply from: '../gradle/versions.gradle'
apply from: rootProject.file('gradle/versions.gradle')
apply from: '../gradle/publish.gradle'

android {
compileSdkVersion compileSdk
buildToolsVersion buildTools
compileSdkVersion rootProject.compileSdk
buildToolsVersion rootProject.buildTools

defaultConfig {
minSdkVersion minSdk
targetSdkVersion targetSdk
minSdkVersion rootProject.minSdk
targetSdkVersion rootProject.targetSdk
versionCode Integer.parseInt(VERSION_CODE)
versionName VERSION_NAME
}
Expand Down
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2.6.0

#### Fixed
#### Changes
`TinkoffAcquiring` constructor should accept `applicationContext` parameter now
#### Additions
Added app-based 3DS flow for payments ([migration](/migration.md))

## 2.5.12

#### Fixed
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,18 @@ class AcquiringSdk(
}
}

fun submit3DSAuthorization(threeDSServerTransID: String, transStatus: String, request: (Submit3DSAuthorizationRequest.() -> Unit)? = null): Submit3DSAuthorizationRequest {
return Submit3DSAuthorizationRequest().apply {
terminalKey = this@AcquiringSdk.terminalKey
this.threeDSServerTransID = threeDSServerTransID
this.transStatus = transStatus
request?.invoke(this)
}
}

class TinkoffPayStatusCache(
val status: TinkoffPayStatusResponse,
val time: Long) {
val status: TinkoffPayStatusResponse,
val time: Long) {

fun isExpired() = System.currentTimeMillis() - time > CACHE_EXPIRE_TIME_MS

Expand Down
20 changes: 16 additions & 4 deletions core/src/main/java/ru/tinkoff/acquiring/sdk/models/ThreeDsData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,36 @@ class ThreeDsData(
) : Serializable {

/**
* Уникальный номер заказа в системе платежного шлюза, для проверки 3DS
* Уникальный номер заказа в системе платежного шлюза, для проверки 3DS (3DS 1.x)
*/
var md: String? = null

/**
* Параметр из ответа на запрос оплаты, для проверки 3DS
* Параметр из ответа на запрос оплаты, для проверки 3DS (3DS 1.x)
*/
var paReq: String? = null

/**
* Идентификатор транзакции из ответа метода
* Идентификатор транзакции из ответа метода (3DS 2.x)
*/
var tdsServerTransId: String? = null

/**
* Идентификатор транзакции, присвоенный ACS
* Идентификатор транзакции, присвоенный ACS (3DS 2.x)
*/
var acsTransId: String? = null

/**
* Идентификатор ACS (3DS 2.1, app-based)
*/
var acsRefNumber: String? = null

/**
* JWT-токен, сфоримарованный ACS для проеведения транзацкии; содержит ACS URL, ACS ephemeral
* public key и SDK ephemeral public key (3DS 2.1, app-based)
*/
var acsSignedContent: String? = null

/**
* Версия протокола 3DS
*/
Expand Down Expand Up @@ -90,6 +101,7 @@ class ThreeDsData(
"paReq = $paReq, " +
"tdsServerTransId = $tdsServerTransId, " +
"acsTransId = $acsTransId, " +
"acsRefNumber = $acsRefNumber, " +
"isThreeDsNeed = $isThreeDsNeed, " +
"version = $version;"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import ru.tinkoff.acquiring.sdk.requests.AcquiringRequest
import ru.tinkoff.acquiring.sdk.requests.FinishAuthorizeRequest
import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse
import ru.tinkoff.acquiring.sdk.responses.GetCardListResponse
import ru.tinkoff.acquiring.sdk.utils.CryptoUtils.sha256
import ru.tinkoff.acquiring.sdk.utils.serialization.*
import java.io.*
import java.lang.reflect.Modifier
Expand Down Expand Up @@ -145,7 +144,7 @@ internal class NetworkClient {
AcquiringApi.API_REQUEST_METHOD_GET -> false
else -> true
}
setRequestProperty("Content-type", if (AcquiringApi.useV1Api(request.apiMethod)) FORM_URL_ENCODED else JSON)
setRequestProperty("Content-type", request.contentType)

if (request is FinishAuthorizeRequest && request.is3DsVersionV2()) {
setRequestProperty("User-Agent", System.getProperty("http.agent"))
Expand All @@ -157,7 +156,7 @@ internal class NetworkClient {
}

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

onReady(requestBody.toByteArray())
Expand Down Expand Up @@ -186,66 +185,6 @@ internal class NetworkClient {
return URL(builder.toString())
}

private fun <R : AcquiringResponse> formatRequestBody(request: AcquiringRequest<R>): String {
val params = if (request.password != null)
enrichWithToken(request)
else
request.asMap()

if (params.isEmpty()) {
return ""
}

return if (AcquiringApi.useV1Api(request.apiMethod)) {
encodeRequestBody(params)
} else {
jsonRequestBody(params)
}
}

private fun jsonRequestBody(params: Map<String, Any>): String {
return gson.toJson(params)
}

private fun <R : AcquiringResponse> enrichWithToken(request: AcquiringRequest<R>): MutableMap<String, Any> {
val token = StringBuilder()

val params = request.asMap()
val sortedKeys = params.keys.sorted()
val ignore = request.tokenIgnoreFields

sortedKeys.forEach {
if (ignore.contains(it))
return@forEach

token.append(params[it])
}

params[AcquiringRequest.TOKEN] = token.toString().sha256()
params.remove(AcquiringRequest.PASSWORD)

return params
}

private fun encodeRequestBody(params: Map<String, Any>): String {
val builder = StringBuilder()
for ((key, value1) in params) {
try {
val value = URLEncoder.encode(value1.toString(), "UTF-8")
builder.append(key)
builder.append('=')
builder.append(value)
builder.append('&')
} catch (e: UnsupportedEncodingException) {
AcquiringSdk.log(e)
}
}

builder.setLength(builder.length - 1)

return builder.toString()
}

@Throws(IOException::class)
private fun read(reader: InputStreamReader): String {
val buffer = CharArray(STREAM_BUFFER_SIZE)
Expand All @@ -271,8 +210,10 @@ internal class NetworkClient {
}
}

private fun createGson(): Gson {
return GsonBuilder()
companion object {

fun createGson(): Gson {
return GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.setExclusionStrategies(SerializableExclusionStrategy())
.registerTypeAdapter(CardStatus::class.java, CardStatusSerializer())
Expand All @@ -281,5 +222,6 @@ internal class NetworkClient {
.registerTypeAdapter(Tax::class.java, TaxSerializer())
.registerTypeAdapter(Taxation::class.java, TaxationSerializer())
.create()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@

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.NetworkClient
import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse
import ru.tinkoff.acquiring.sdk.utils.Request
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.security.PublicKey

/**
Expand All @@ -29,7 +33,10 @@ import java.security.PublicKey
*/
abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: String) : Request<R> {

protected val gson: Gson = NetworkClient.createGson()

open val httpRequestMethod: String = AcquiringApi.API_REQUEST_METHOD_POST
open val contentType: String = AcquiringApi.JSON

internal lateinit var terminalKey: String
internal lateinit var publicKey: PublicKey
Expand Down Expand Up @@ -88,6 +95,39 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
}
}

open fun getRequestBody(): String {
val params = asMap()
if (params.isEmpty()) return ""

return when (contentType) {
AcquiringApi.FORM_URL_ENCODED -> encodeRequestBody(params)
else -> jsonRequestBody(params)
}
}

protected open fun jsonRequestBody(params: Map<String, Any>): String {
return gson.toJson(params)
}

protected fun encodeRequestBody(params: Map<String, Any>): String {
val builder = StringBuilder()
for ((key, value1) in params) {
try {
val value = URLEncoder.encode(value1.toString(), "UTF-8")
builder.append(key)
builder.append('=')
builder.append(value)
builder.append('&')
} catch (e: UnsupportedEncodingException) {
AcquiringSdk.log(e)
}
}

builder.setLength(builder.length - 1)

return builder.toString()
}

internal companion object {

const val TERMINAL_KEY = "TerminalKey"
Expand Down Expand Up @@ -125,5 +165,8 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
const val IP = "IP"
const val CONNECTION_TYPE = "connection_type"
const val SDK_VERSION = "sdk_version"
const val THREE_DS_SERVER_TRANS_ID = "threeDSServerTransID"
const val TRANS_STATUS = "transStatus"
const val CRES = "cres"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class FinishAuthorizeRequest : AcquiringRequest<FinishAuthorizeResponse>(FINISH_
}

private fun MutableMap<String, Any>.putDataIfNonNull(data: Map<String, String>?) {
if (data != null) {
if (!data.isNullOrEmpty()) {
this[DATA] = data.toMutableMap()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright © 2020 Tinkoff Bank
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ru.tinkoff.acquiring.sdk.requests

import ru.tinkoff.acquiring.sdk.network.AcquiringApi
import ru.tinkoff.acquiring.sdk.responses.Submit3DSAuthorizationResponse
import ru.tinkoff.acquiring.sdk.utils.Base64

/**
* Подтверждает прохождение app-based 3DS версии 2.1
*/
class Submit3DSAuthorizationRequest : AcquiringRequest<Submit3DSAuthorizationResponse>(
AcquiringApi.SUBMIT_3DS_AUTHORIZATION_V2) {

override val contentType: String = AcquiringApi.FORM_URL_ENCODED

/**
* Уникальный идентификатор транзакции, генерируемый 3DS-Server
*/
var threeDSServerTransID: String? = null

/**
* Статус транзакции
*/
var transStatus: String? = null

override fun asMap(): MutableMap<String, Any> {
val cres = gson.toJson(buildMap {
this[THREE_DS_SERVER_TRANS_ID] = threeDSServerTransID!!
this[TRANS_STATUS] = transStatus!!
})
val encodedCres = Base64.encodeToString(cres.toByteArray(), Base64.NO_WRAP)
return mutableMapOf(CRES to encodedCres)
}

override fun validate() {
threeDSServerTransID.validate(THREE_DS_SERVER_TRANS_ID)
transStatus.validate(TRANS_STATUS)
}

/**
* Синхронный вызов метода API
*/
override fun execute(onSuccess: (Submit3DSAuthorizationResponse) -> Unit, onFailure: (Exception) -> Unit) {
super.performRequest(this, Submit3DSAuthorizationResponse::class.java, onSuccess, onFailure)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.google.gson.annotations.SerializedName
* обязательный параметр для 3DS второй версии
* @param threeDsMethodUrl дополнительный параметр для 3DS второй версии, который позволяет
* пройти этап по сбору данных браузера ACS-ом
* @param paymentSystem платежная система, через которую будет проводится оплата, участвует
* в прохождении 3DS по app-based flow
*
* @author Mariya Chernyadieva
*/
Expand All @@ -37,6 +39,9 @@ class Check3dsVersionResponse(
val serverTransId: String? = null,

@SerializedName("ThreeDSMethodURL")
val threeDsMethodUrl: String? = null
val threeDsMethodUrl: String? = null,

@SerializedName("PaymentSystem")
val paymentSystem: String? = null

) : AcquiringResponse()
Loading

0 comments on commit 1dfa379

Please sign in to comment.