Skip to content

Commit

Permalink
Merge pull request #103 from Tinkoff/2.7.0
Browse files Browse the repository at this point in the history
2.7.0 Add token to acquiring requests
  • Loading branch information
IlnarH authored Aug 22, 2022
2 parents 1dfa379 + b25d324 commit 56c789a
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 24 deletions.
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2.7.0

#### Fixed
#### Changes
`TinkoffAcquiring` constructor should accept `tokenGenerator` parameter now ([migration](/migration.md))
#### Additions

## 2.6.0

#### Fixed
Expand Down
43 changes: 42 additions & 1 deletion core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ import ru.tinkoff.acquiring.sdk.loggers.JavaLogger
import ru.tinkoff.acquiring.sdk.loggers.Logger
import ru.tinkoff.acquiring.sdk.requests.*
import ru.tinkoff.acquiring.sdk.responses.TinkoffPayStatusResponse
import ru.tinkoff.acquiring.sdk.utils.SampleAcquiringTokenGenerator
import ru.tinkoff.acquiring.sdk.utils.keycreators.KeyCreator
import ru.tinkoff.acquiring.sdk.utils.keycreators.StringKeyCreator
import java.security.MessageDigest
import java.security.PublicKey

/**
* Класс позволяет конфигурировать SDK и осуществлять взаимодействие с Tinkoff Acquiring API.
* Методы осуществляют обращение к API.
* Вызов методов выполняется синхронно
*
* Для корректного выполнения запросов также необходимо указать [tokenGenerator].
*
* @param terminalKey ключ терминала. Выдается после подключения к Tinkoff Acquiring
* @param publicKey экземпляр PublicKey созданный из публичного ключа, выдаваемого вместе с
* terminalKey
Expand Down Expand Up @@ -243,7 +247,13 @@ class AcquiringSdk(
}
}

companion object AsdkLogger {
companion object {

/**
* Объект, который будет использоваться для генерации токена при формировании запросов к api
* ([документация по формированию токена](https://www.tinkoff.ru/kassa/develop/api/request-sign/))
*/
var tokenGenerator: AcquiringTokenGenerator? = null

/**
* Позволяет использовать свой логгер или заданный
Expand Down Expand Up @@ -280,4 +290,35 @@ class AcquiringSdk(
}
}
}
}

/**
* Объект, который будет использоваться для генерации токена при формировании
* запросов к api ([документация по формированию токена](https://www.tinkoff.ru/kassa/develop/api/request-sign/)).
* На вход принимает словарь параметров (объекты **Shops**, **Receipt** и **DATA** уже исключены из
* этого словаря), на выходе должен вернуть строку, являющуюся токеном.
*
* Алгоритм формирования токена:
* 1. Добавить в исходный словарь пароль терминала с ключом **Password**.
* 2. Отсортировать словарь по ключам в алфавитном порядке.
* 3. Конкатенировать значения всех пар.
* 4. Для полученной строки вычислить хэш SHA-256.
*
* Полученный хэш и будет являться токеном.
*
* Пример реализации алгоритма генерации токена можно увидеть в [SampleAcquiringTokenGenerator].
*
* **Note:** Метод вызывается в фоновом потоке.
*/
fun interface AcquiringTokenGenerator {

fun generateToken(request: AcquiringRequest<*>, params: MutableMap<String, Any>): String

companion object {

fun sha256hashString(source: String): String =
MessageDigest.getInstance("SHA-256")
.digest(source.toByteArray())
.joinToString("") { "%02x".format(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
private var disposed = false
private val ignoredFieldsSet: HashSet<String> = hashSetOf(DATA, RECEIPT, RECEIPTS, SHOPS)

public var password: String? = null

internal open val tokenIgnoreFields: HashSet<String>
get() = ignoredFieldsSet

Expand All @@ -64,7 +62,6 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
val map = HashMap<String, Any>()

map.putIfNotNull(TERMINAL_KEY, terminalKey)
map.putIfNotNull(PASSWORD, password)

return map
}
Expand Down Expand Up @@ -99,17 +96,27 @@ abstract class AcquiringRequest<R : AcquiringResponse>(internal val apiMethod: S
val params = asMap()
if (params.isEmpty()) return ""

getToken()?.let { params[TOKEN] = it }

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

protected open fun jsonRequestBody(params: Map<String, Any>): String {
return gson.toJson(params)
protected open fun getToken(): String? =
AcquiringSdk.tokenGenerator?.generateToken(this, paramsForToken())

private fun paramsForToken(): MutableMap<String, Any> {
val tokenParams = asMap()
tokenIgnoreFields.forEach {
tokenParams.remove(it)
}
tokenParams.remove(TOKEN)
return tokenParams
}

protected fun encodeRequestBody(params: Map<String, Any>): String {
private fun encodeRequestBody(params: Map<String, Any>): String {
val builder = StringBuilder()
for ((key, value1) in params) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class TinkoffPayLinkRequest(paymentId: String, version: String) :

override fun asMap(): MutableMap<String, Any> = mutableMapOf()

override fun getToken(): String? = null

override fun validate() = Unit

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class TinkoffPayStatusRequest(terminalKey: String) :

override fun asMap(): MutableMap<String, Any> = mutableMapOf()

override fun getToken(): String? = null

override fun validate() = Unit

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.tinkoff.acquiring.sdk.utils

import ru.tinkoff.acquiring.sdk.AcquiringTokenGenerator
import ru.tinkoff.acquiring.sdk.requests.AcquiringRequest
import java.lang.StringBuilder

/**
* @param password пароль терминала, который будет использоваться при формировании токена.
* **В целях безопасности пароль не рекомендуется хранить в коде Android-приложения.**
*/
class SampleAcquiringTokenGenerator(private val password: String) : AcquiringTokenGenerator {

override fun generateToken(request: AcquiringRequest<*>, params: MutableMap<String, Any>): String {
params[AcquiringRequest.PASSWORD] = password
val sorted = params.toSortedMap()

val token = StringBuilder()
sorted.values.forEach { token.append(it) }
return AcquiringTokenGenerator.sha256hashString(token.toString())
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION_NAME=2.6.0
VERSION_NAME=2.7.0
VERSION_CODE=15
GROUP=ru.tinkoff.acquiring

Expand Down
13 changes: 12 additions & 1 deletion migration.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
2.7.0

Большинство запросов теперь требует передачи токена.

Конструктор `TinkoffAcquiring` теперь последним параметром принимает `tokenGenerator` - объект,
используя который SDK будет генерировать токен для передачи в запросах (см. kDoc `AcquiringTokenGenerator`);
пример реализации алгоритма генерации токена можно посмотреть в `SampleAcquiringTokenGenerator`.

При использовании зависимости только от `core`-модуля объект для генерации токена можно задать
через `AcquiringSdk.tokenGenerator`.

2.6.0

Добавление подтверждение 3DS по app-based flow при проведении платежа.

Конструктор `TinkoffAcquiring` теперь первым параметром принимает `applicationContext`.

Кроме того, для корректной работы SDK, при использовании зависимости от `ui` модуля в
Кроме того, для корректной работы SDK, при использовании зависимости от `ui`-модуля в
Android-приложениях, также следует добавить дополнительные зависимости:

```groovy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager
import ru.tinkoff.acquiring.sdk.AcquiringSdk
import ru.tinkoff.acquiring.sdk.TinkoffAcquiring
import ru.tinkoff.acquiring.sdk.payment.PaymentProcess
import ru.tinkoff.acquiring.sdk.utils.SampleAcquiringTokenGenerator

/**
* @author Mariya Chernyadieva
Expand All @@ -33,7 +34,8 @@ class SampleApplication : Application() {

val settings = SettingsSdkManager(this)
val params = SessionParams[settings.terminalKey]
tinkoffAcquiring = TinkoffAcquiring(this, params.terminalKey, params.publicKey)
tinkoffAcquiring = TinkoffAcquiring(this, params.terminalKey,
params.publicKey, SampleAcquiringTokenGenerator(params.password))
AcquiringSdk.isDeveloperMode = true
AcquiringSdk.isDebug = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.util.*
*/
class SessionParams(
val terminalKey: String,
val password: String,
val publicKey: String,
val customerKey: String,
val customerEmail: String) {
Expand All @@ -37,6 +38,7 @@ class SessionParams(
private const val SDK_TERMINAL_ID = "TestSDK"
private const val NON_3DS_TERMINAL_ID = "sdkNon3DS"

private const val PASSWORD = "12345678"
private const val PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Yg3RyEkszggDVMDHCAG\n" +
"zJm0mYpYT53BpasrsKdby8iaWJVACj8ueR0Wj3Tu2BY64HdIoZFvG0v7UqSFztE/\n" +
"zUvnznbXVYguaUcnRdwao9gLUQO2I/097SHF9r++BYI0t6EtbbcWbfi755A1EWfu\n" +
Expand All @@ -46,11 +48,11 @@ class SessionParams(
"jwIDAQAB"

val TEST_SDK = SessionParams(
SDK_TERMINAL_ID, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL
SDK_TERMINAL_ID, PASSWORD, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL
)

val NON_3DS = SessionParams(
NON_3DS_TERMINAL_ID, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL
NON_3DS_TERMINAL_ID, PASSWORD, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL
)

val DEFAULT = TEST_SDK
Expand Down
18 changes: 14 additions & 4 deletions ui/src/main/java/ru/tinkoff/acquiring/sdk/TinkoffAcquiring.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,25 @@ import ru.tinkoff.acquiring.sdk.ui.activities.ThreeDsActivity
/**
* Точка входа для взаимодействия с Acquiring SDK
*
* @param terminalKey ключ терминала. Выдается после подключения к Tinkoff Acquiring
* @param publicKey экземпляр PublicKey созданный из публичного ключа, выдаваемого вместе с
* terminalKey
* @param terminalKey ключ терминала. Выдается после подключения к Tinkoff Acquiring
* @param publicKey экземпляр PublicKey созданный из публичного ключа, выдаваемого вместе с
* terminalKey
* @param tokenGenerator объект, который будет использоваться для генерации токена при формировании
* запросов к api ([документация по формированию токена](https://www.tinkoff.ru/kassa/develop/api/request-sign/))
*
*
* @author Mariya Chernyadieva
*/
class TinkoffAcquiring(
private val applicationContext: Context,
private val terminalKey: String,
private val publicKey: String
private val publicKey: String,
tokenGenerator: AcquiringTokenGenerator
) {
init {
TinkoffAcquiring.tokenGenerator = tokenGenerator
}

val sdk = AcquiringSdk(terminalKey, publicKey)

/**
Expand Down Expand Up @@ -453,5 +461,7 @@ class TinkoffAcquiring(
const val EXTRA_REBILL_ID = "extra_rebill_id"

const val EXTRA_CARD_LIST_CHANGED = "extra_cards_changed"

var tokenGenerator: AcquiringTokenGenerator? by AcquiringSdk.Companion::tokenGenerator
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import ru.tinkoff.acquiring.sdk.requests.InitRequest
import ru.tinkoff.acquiring.sdk.responses.ChargeResponse
import ru.tinkoff.acquiring.sdk.responses.Check3dsVersionResponse
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper.cleanupSafe
import ru.tinkoff.acquiring.sdk.utils.CoroutineManager
import ru.tinkoff.acquiring.sdk.utils.getIpAddress
import ru.tinkoff.core.components.threedswrapper.ThreeDSWrapper
Expand Down Expand Up @@ -323,7 +324,7 @@ internal constructor(
): Transaction? {
val dsId = ThreeDsHelper.getDsId(paymentSystem)
if (dsId == null) {
threeDSWrapper.cleanup(context)
threeDSWrapper.cleanupSafe(context)
handleException(AcquiringSdkException(IllegalArgumentException(
"Directory server ID for payment system \"$paymentSystem\" can't be found")))
return null
Expand All @@ -347,7 +348,7 @@ internal constructor(

} catch (e: Throwable) {
transaction?.closeSafe()
threeDSWrapper.cleanup(context)
threeDSWrapper.cleanupSafe(context)
handleException(e)
return null
}
Expand Down
10 changes: 10 additions & 0 deletions ui/src/main/java/ru/tinkoff/acquiring/sdk/threeds/ThreeDsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,14 @@ object ThreeDsHelper {
true -> CERTS_CONFIG_URL_TEST
else -> CERTS_CONFIG_URL_PROD
}

fun ThreeDSWrapper.cleanupSafe(context: Context) {
if (isInitialized()) {
try {
cleanup(context)
} catch (ignored: Throwable) {
// ignore
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import ru.tinkoff.acquiring.sdk.models.result.AsdkResult
import ru.tinkoff.acquiring.sdk.models.result.CardResult
import ru.tinkoff.acquiring.sdk.models.result.PaymentResult
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper.cleanupSafe
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusCanceled
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusError
import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusSuccess
Expand Down Expand Up @@ -70,33 +71,33 @@ internal class ThreeDsViewModel(
object : ChallengeStatusReceiverAdapter(transaction, progressDialog) {
override fun completed(event: CompletionEvent?) {
super.completed(event)
wrapper.cleanup(activity)
wrapper.cleanupSafe(activity)
ThreeDsHelper.threeDsStatus = ThreeDsStatusSuccess(threeDsData, event!!.transactionStatus)
}

override fun cancelled() {
super.cancelled()
wrapper.cleanup(activity)
wrapper.cleanupSafe(activity)
ThreeDsHelper.threeDsStatus = ThreeDsStatusCanceled()
}

override fun timedout() {
super.timedout()
wrapper.cleanup(activity)
wrapper.cleanupSafe(activity)
val error = RuntimeException("3DS SDK transaction timeout")
ThreeDsHelper.threeDsStatus = ThreeDsStatusError(error)
}

override fun protocolError(event: ProtocolErrorEvent?) {
super.protocolError(event)
wrapper.cleanup(activity)
wrapper.cleanupSafe(activity)
val error = RuntimeException("3DS SDK protocol error: sdkTransactionID - ${event?.sdkTransactionID}, message - ${event?.errorMessage}")
ThreeDsHelper.threeDsStatus = ThreeDsStatusError(error)
}

override fun runtimeError(event: RuntimeErrorEvent?) {
super.runtimeError(event)
wrapper.cleanup(activity)
wrapper.cleanupSafe(context)
val error = RuntimeException("3DS SDK runtime error: code - ${event?.errorCode}, message - ${event?.errorMessage}")
ThreeDsHelper.threeDsStatus = ThreeDsStatusError(error)
}
Expand Down

0 comments on commit 56c789a

Please sign in to comment.