diff --git a/cardio/build.gradle b/cardio/build.gradle index 9424d30e..60631890 100644 --- a/cardio/build.gradle +++ b/cardio/build.gradle @@ -38,6 +38,4 @@ dependencies { implementation project(':ui') testImplementation 'junit:junit:4.13' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } diff --git a/core/build.gradle b/core/build.gradle index 9d20c379..d8330905 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${okHttpVersion}" implementation "com.google.code.gson:gson:$gsonVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" testImplementation 'junit:junit:4.13' } diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt index 22fd3415..ab9bfb6b 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt @@ -226,6 +226,10 @@ class AcquiringSdk( } } + fun getTerminalPayMethods() : GetTerminalPayMethodsRequest { + return GetTerminalPayMethodsRequest(terminalKey) + } + fun submit3DSAuthorization(threeDSServerTransID: String, transStatus: String, request: (Submit3DSAuthorizationRequest.() -> Unit)? = null): Submit3DSAuthorizationRequest { return Submit3DSAuthorizationRequest().apply { terminalKey = this@AcquiringSdk.terminalKey diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/YandexPay.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/YandexPay.kt new file mode 100644 index 00000000..144d7831 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/YandexPay.kt @@ -0,0 +1,12 @@ +package ru.tinkoff.acquiring.sdk.models.paysources + +import ru.tinkoff.acquiring.sdk.models.PaymentSource + +/** + * Тип оплаты с помощью Yandex Pay + * + * @param yandexPayToken токен для оплаты, полученный через Yandex Pay + * + * Created by i.golovachev + */ +class YandexPay(var yandexPayToken: String) : PaymentSource \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt index a7d7200b..d3cb081c 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt @@ -43,6 +43,7 @@ object AcquiringApi { const val SUBMIT_3DS_AUTHORIZATION = "Submit3DSAuthorization" const val SUBMIT_3DS_AUTHORIZATION_V2 = "Submit3DSAuthorizationV2" const val COMPLETE_3DS_METHOD_V2 = "Complete3DSMethodv2" + const val GET_TERMINAL_PAY_METHODS = "GetTerminalPayMethods" const val API_ERROR_CODE_3DSV2_NOT_SUPPORTED = "106" const val API_ERROR_CODE_CUSTOMER_NOT_FOUND = "7" diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/network/NetworkClient.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/network/NetworkClient.kt index 6307b957..efc3d00c 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/network/NetworkClient.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/network/NetworkClient.kt @@ -189,6 +189,7 @@ internal class NetworkClient { fun createGson(): Gson { return GsonBuilder() + .disableHtmlEscaping() .excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC) .setExclusionStrategies(SerializableExclusionStrategy()) .registerTypeAdapter(CardStatus::class.java, CardStatusSerializer()) diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt index f09c0774..902aeefc 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt @@ -17,6 +17,8 @@ package ru.tinkoff.acquiring.sdk.requests import com.google.gson.Gson +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import okhttp3.Response import ru.tinkoff.acquiring.sdk.AcquiringSdk import ru.tinkoff.acquiring.sdk.exceptions.NetworkException @@ -34,7 +36,8 @@ import java.security.PublicKey * * @author Mariya Chernyadieva, Taras Nagorny */ -abstract class AcquiringRequest(internal val apiMethod: String) : Request { +abstract class AcquiringRequest(internal val apiMethod: String) : + Request { protected val gson: Gson = NetworkClient.createGson() @@ -43,6 +46,7 @@ abstract class AcquiringRequest(internal val apiMethod: S internal lateinit var terminalKey: String internal lateinit var publicKey: PublicKey + @Volatile private var disposed = false private val ignoredFieldsSet: HashSet = hashSetOf(DATA, RECEIPT, RECEIPTS, SHOPS) @@ -51,7 +55,6 @@ abstract class AcquiringRequest(internal val apiMethod: S internal open val tokenIgnoreFields: HashSet get() = ignoredFieldsSet - protected abstract fun validate() override fun isDisposed(): Boolean { @@ -81,6 +84,28 @@ abstract class AcquiringRequest(internal val apiMethod: S client.call(request, responseClass, onSuccess, onFailure) } + open fun performRequestAsync(responseClass: Class): Deferred> { + this.validate() + val client = NetworkClient() + val deferred: CompletableDeferred> = CompletableDeferred() + + client.call(this, responseClass, + onSuccess = { + deferred.complete(Result.success(it)) + }, + onFailure = { + deferred.complete(Result.failure(it)) + }) + return deferred + } + + suspend fun performSuspendRequest(responseClass: Class): Result { + return performRequestAsync(responseClass).run { + start() + await() + } + } + @kotlin.jvm.Throws(NetworkException::class) protected fun performRequestRaw(request: AcquiringRequest): Response { request.validate() @@ -188,7 +213,7 @@ abstract class AcquiringRequest(internal val apiMethod: S const val REQUEST_KEY = "RequestKey" const val SOURCE = "Source" const val PAYMENT_SOURCE = "PaymentSource" - const val ANDROID_PAY_TOKEN = "EncryptedPaymentData" + const val ENCRYPTED_PAYMENT_DATA = "EncryptedPaymentData" const val DATA_TYPE = "DataType" const val REDIRECT_DUE_DATE = "RedirectDueDate" const val NOTIFICATION_URL = "NotificationURL" @@ -202,5 +227,10 @@ abstract class AcquiringRequest(internal val apiMethod: S const val THREE_DS_SERVER_TRANS_ID = "threeDSServerTransID" const val TRANS_STATUS = "transStatus" const val CRES = "cres" + const val PAYSOURCE = "Paysource" } } + +suspend inline fun AcquiringRequest.performSuspendRequest(): Result { + return performSuspendRequest(R::class.java) +} \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/FinishAuthorizeRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/FinishAuthorizeRequest.kt index 095b72d7..91cacb83 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/FinishAuthorizeRequest.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/FinishAuthorizeRequest.kt @@ -16,12 +16,10 @@ package ru.tinkoff.acquiring.sdk.requests +import kotlinx.coroutines.Deferred import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.PaymentSource -import ru.tinkoff.acquiring.sdk.models.paysources.AttachedCard -import ru.tinkoff.acquiring.sdk.models.paysources.CardData -import ru.tinkoff.acquiring.sdk.models.paysources.CardSource -import ru.tinkoff.acquiring.sdk.models.paysources.GooglePay +import ru.tinkoff.acquiring.sdk.models.paysources.* import ru.tinkoff.acquiring.sdk.network.AcquiringApi.FINISH_AUTHORIZE_METHOD import ru.tinkoff.acquiring.sdk.responses.FinishAuthorizeResponse @@ -71,7 +69,7 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ private var cardId: String? = null private var cvv: String? = null private var source: String? = null - private var googlePayToken: String? = null + private var encryptedToken: String? = null private var encodedCardData: String? = null override val tokenIgnoreFields: HashSet @@ -93,7 +91,7 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ map.putIfNotNull(CVV, cvv) map.putIfNotNull(EMAIL, email) map.putIfNotNull(SOURCE, source) - map.putIfNotNull(ANDROID_PAY_TOKEN, googlePayToken) + map.putIfNotNull(ENCRYPTED_PAYMENT_DATA, encryptedToken) map.putIfNotNull(IP, ip) if (data != null) map.putDataIfNonNull(data) @@ -106,7 +104,7 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ when (paymentSource) { is CardData, is AttachedCard -> encodedCardData.validate(CARD_DATA) - is GooglePay -> googlePayToken.validate(ANDROID_PAY_TOKEN) + is GooglePay, is YandexPay -> encryptedToken.validate(ENCRYPTED_PAYMENT_DATA) } } @@ -118,6 +116,11 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ super.performRequest(this, FinishAuthorizeResponse::class.java, onSuccess, onFailure) } + override fun performRequestAsync(responseClass: Class): Deferred> { + fillPaymentData() + return super.performRequestAsync(responseClass) + } + fun attachedCard(attachedCard: AttachedCard.() -> Unit): PaymentSource { return AttachedCard().apply(attachedCard) } @@ -143,10 +146,14 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ } is GooglePay -> { data = paymentSource as GooglePay - this.googlePayToken = data.googlePayToken + this.encryptedToken = data.googlePayToken this.source = GOOGLE_PAY } - + is YandexPay ->{ + data = paymentSource as YandexPay + this.encryptedToken = data.yandexPayToken + this.source = YANDEX_PAY + } else -> throw AcquiringSdkException(IllegalStateException("Unknown type in 'paymentSource'")) } } @@ -160,5 +167,6 @@ class FinishAuthorizeRequest : AcquiringRequest(FINISH_ companion object { private const val GOOGLE_PAY = "GooglePay" + private const val YANDEX_PAY = "YandexPay" } } diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetTerminalPayMethodsRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetTerminalPayMethodsRequest.kt new file mode 100644 index 00000000..1dea42f8 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetTerminalPayMethodsRequest.kt @@ -0,0 +1,37 @@ +package ru.tinkoff.acquiring.sdk.requests + +import ru.tinkoff.acquiring.sdk.network.AcquiringApi +import ru.tinkoff.acquiring.sdk.network.AcquiringApi.GET_TERMINAL_PAY_METHODS +import ru.tinkoff.acquiring.sdk.responses.GetTerminalPayMethodsResponse + +/** + * Запрос в MAPI, проверяет доступности методов оплаты на терминале + * + * Created by Ivan Golovachev + */ +class GetTerminalPayMethodsRequest( + terminalKey: String, + paysource: Paysource = Paysource.SDK +) : + AcquiringRequest( + "$GET_TERMINAL_PAY_METHODS?TerminalKey=$terminalKey&PaySource=$paysource") { + + override val httpRequestMethod: String = AcquiringApi.API_REQUEST_METHOD_GET + + override fun validate() = Unit + + override fun asMap(): MutableMap = mutableMapOf() + + override fun getToken(): String? = null + + override fun execute( + onSuccess: (GetTerminalPayMethodsResponse) -> Unit, + onFailure: (Exception) -> Unit + ) { + super.performRequest(this, GetTerminalPayMethodsResponse::class.java, onSuccess, onFailure) + } + + enum class Paysource { + API, SDK + } +} \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/FinishAuthorizeResponse.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/FinishAuthorizeResponse.kt index 1121e6d4..585e2c0d 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/FinishAuthorizeResponse.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/FinishAuthorizeResponse.kt @@ -89,7 +89,7 @@ class FinishAuthorizeResponse( ThreeDsData(paymentId, acsUrl).apply { md = this@FinishAuthorizeResponse.md paReq = this@FinishAuthorizeResponse.paReq - version = threeDsVersion + version = threeDsVersion ?: "1.0.0" } } else if (tdsServerTransId != null && acsTransId != null) { ThreeDsData(paymentId, acsUrl).apply { @@ -97,7 +97,7 @@ class FinishAuthorizeResponse( acsTransId = this@FinishAuthorizeResponse.acsTransId acsRefNumber = this@FinishAuthorizeResponse.acsRefNumber acsSignedContent = this@FinishAuthorizeResponse.acsSignedContent - version = threeDsVersion + version = threeDsVersion ?: "2.1.0" } } else throw AcquiringSdkException(IllegalStateException("Invalid 3DS params")) } diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt new file mode 100644 index 00000000..34da66fb --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt @@ -0,0 +1,68 @@ +package ru.tinkoff.acquiring.sdk.responses + +import com.google.gson.annotations.SerializedName + +/** + * Ответ на запрос /v2/GetTerminalPayMethods + * + * @param terminalInfo - Характеристики терминала + * + * + * Created by Ivan Golovachev + */ +class GetTerminalPayMethodsResponse( + + @SerializedName("TerminalInfo") + val terminalInfo: TerminalInfo? = null + +) : AcquiringResponse() + + +/** + * + * @param terminalInfo - Характеристики терминала + * @param paymethods - Перечень доступных методов оплаты + * @param addCardScheme - Признак возможности сохранения карт + * @param tokenRequired - Признак необходимости подписания токеном + * @param initTokenRequired - Признак необходимости подписания токеном запроса /init + * + * + * Created by Ivan Golovachev + */ +class TerminalInfo( + + @SerializedName("Paymethods") + val paymethods: List = emptyList(), + + @SerializedName("AddCardScheme") + val addCardScheme: Boolean = false, + + @SerializedName("TokenRequired") + val tokenRequired: Boolean = true, + + @SerializedName("InitTokenRequired") + val initTokenRequired: Boolean = false +) + +/** + * @param params - Перечень параметров подключения в формате ключ-значение + */ +class PaymethodData( + + @SerializedName("PayMethod") + val paymethod: Paymethod? = null, + + @SerializedName("Params") + val params: Map = emptyMap() +) + +enum class Paymethod { + @SerializedName("TinkoffPay") + TinkoffPay, + + @SerializedName("YandexPay") + YandexPay, + + @SerializedName("SBP") + SBP +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index aaad050c..71d1fc98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,8 @@ POM_LICENCE_URL=https://github.com/Tinkoff/AcquiringSdkAndroid/blob/master/LICEN POM_LICENCE_DIST=repo POM_DEVELOPER_ID=tcs POM_DEVELOPER_NAME=Tinkoff Credit Systems +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx8096m -XX:+UseConcMarkSweepGC -Dfile.encoding=UTF-8 android.useAndroidX=true android.enableJetifier=true diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 3ed8293e..30fc6657 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -4,8 +4,9 @@ ext { compileSdk = 31 minSdk = 21 + yandexPayMinSdk = 23 targetSdk = 31 - buildTools = '29.0.2' + buildTools = '30.0.3' kotlinVersion = '1.6.10' gradlePluginVersion = '7.1.2' @@ -29,6 +30,9 @@ ext { blurryVersion = '4.0.0' bouncyCastleVersion = '1.65' rootBeerVersion = '0.1.0' + materialVersion = '1.5.0' mokitoKotlin = '4.0.0' + + yandexPayVersion = '0.2.1' } diff --git a/readme.md b/readme.md index 3ae2f2d5..2c9dbddf 100644 --- a/readme.md +++ b/readme.md @@ -117,8 +117,6 @@ tinkoffAcquiring.openPaymentScreen(this@MainActivity, paymentOptions, PAYMENT_RE [2] _Безопасная клавиатура_ используется вместо системной и обеспечивает дополнительную безопасность ввода, т.к. сторонние клавиатуры на устройстве клиента могут перехватывать данные и отправлять их злоумышленнику. -[3] _Безопасная клавиатура_ используется вместо системной и обеспечивает дополнительную безопасность ввода, т.к. сторонние клавиатуры на устройстве клиента могут перехватывать данные и отправлять их злоумышленнику. - ### Экран привязки карт Для запуска экрана привязки карт необходимо запустить **TinkoffAcquiring**#_openAttachCardScreen_. В метод также необходимо передать некоторые параметры - тип привязки, данные покупателя и опционально параметры кастомизации (по-аналогии с экраном оплаты): @@ -254,6 +252,35 @@ var paymentOptions = PaymentOptions().setOptions { предыдущем шаге); время и частота проверки статуса платежа зависит от нужд клиентского приложения и остается на ваше усмотрение (один из вариантов - проверять статус платежа при возвращении приложения из фона) +### Yandex Pay +AcquiringSdk имеет возможность использовать внутри себя Yandex Pay в качестве хранилища карт. +Внимание, этот функционал поддерживается только с версии Android 6.0 Marshmallow и выше. + +Если вы хотите использовать Yandex Pay вместе с AcquiringSdk вам необходимо: +1. Получить в личном кабинете [Yandex](https://pay.yandex.ru/ru/docs/psp/android-sdk) значение `YANDEX_CLIENT_ID` +2. Укажите полученный `YANDEX_CLIENT_ID` в сборочном скрипте [_build.gradle_][build-config] в качестве значения в `manifestPlaceholders`: +```groovy +android { + defaultConfig { + manifestPlaceholders = [ + // Подставьте ваш yandex_client_id + YANDEX_CLIENT_ID: "12345678901234567890", + ] + } +} +``` +3. Добавить в [_build.gradle_][build-config] +```groovy +implementation 'ru.tinkoff.acquiring:yandexpay:$latestVersion' +``` +Крайне не рекомендуется использовать `ru.tinkoff.acquiring:yandexpay` вместе с `com.yandex.pay:core` в рамках вашего приложения, так как +могут возникнуть непредвиденные ошибки. + +4. Включить прием платежей через Yandex Pay в Личном кабинете. +5. Проверить Доступ функционала Yandex Pay проверяется через метод `TinkoffAcquiring#checkTerminalInfo`, который возвращает данные обо всех методах оплаты,извлечь данные касательно Yandex Pay расширение `TerminalInfo#mapYandexPayData`. +6. Кнопка Yandex Pay инкапсулирована во фрагменте `YandexButtonFragment`. Размеры фрагмента-кнопки можете создать самостоятельно, однако если рекомендации по минимальной ширине. Фрагмент можно создать с помощью метода `TinkoffAcquiring.createYandexPayButtonFragment`. +После выбора карты процесс оплаты запуститься самостоятельно. Возможности кастомизации можно посмотреть в [pages](https://github.com/Tinkoff/AcquiringSdkAndroid/wiki/Yandex-pay-in-ASDK). + ### Дополнительные возможности #### Настройка стилей @@ -336,6 +363,7 @@ implementation 'ru.tinkoff.acquiring:core:$latestVersion' -keep class ru.tinkoff.acquiring.sdk.localization.** { *; } -keep class ru.tinkoff.acquiring.sdk.requests.** { *; } -keep class ru.tinkoff.acquiring.sdk.models.** { *; } +-keep class ru.tinkoff.acquiring.sdk.yandexpay.models.** { *; } // если подключали яндекс -keep class ru.rtln.tds.sdk.** { *; } -keep class org.spongycastle.** -keep class org.bouncycastle.** diff --git a/sample/build.gradle b/sample/build.gradle index b1b45d67..fbec59c7 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,12 +7,24 @@ android { compileSdkVersion rootProject.compileSdk buildToolsVersion rootProject.buildTools + signingConfigs { + debug { + storeFile file("debug.keystore") + storePassword 'android' + keyAlias 'androidDebugKey' + keyPassword 'android' + } + } + defaultConfig { applicationId "ru.tinkoff.acquiring.sample" - minSdkVersion rootProject.minSdk + minSdkVersion rootProject.yandexPayMinSdk targetSdkVersion rootProject.compileSdk versionCode Integer.parseInt(VERSION_CODE) versionName VERSION_NAME + manifestPlaceholders = [ + YANDEX_CLIENT_ID: "9dc6814e39204c638222dede9561ea6f" + ] } buildTypes { release { @@ -39,6 +51,7 @@ dependencies { implementation "androidx.preference:preference:$preferenceVersion" implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "com.google.android.material:material:${materialVersion}" implementation "com.google.code.gson:gson:$gsonVersion" testImplementation 'junit:junit:4.13' @@ -46,6 +59,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation project(':ui') + implementation project(':yandexpay') implementation project(':cardio') implementation project(':threeds-sdk') implementation project(':threeds-wrapper') diff --git a/sample/debug.keystore b/sample/debug.keystore new file mode 100644 index 00000000..d1e38090 Binary files /dev/null and b/sample/debug.keystore differ diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index d3dbe361..ca8db8c7 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - @@ -42,6 +43,12 @@ android:theme="@style/AcquiringTheme" android:windowSoftInputMode="adjustNothing" /> + + Unit, + onFailure: ((Throwable) -> Unit)? = null) { + + val onFailureOrThrow = onFailure ?: { throw it } + + CoroutineScope(Dispatchers.IO).launch { + + val result = sdk.getTerminalPayMethods() + .performSuspendRequest() + .map { it.terminalInfo } + + launch(Dispatchers.Main) { + result.fold(onSuccess, onFailureOrThrow) + } + } + } + /** * Запуск SDK для оплаты через Tinkoff Pay. У возвращенгого объекта следует указать * слушатель событий с помощью метода [PaymentProcess.subscribe] и вызвать метод @@ -217,6 +249,7 @@ class TinkoffAcquiring( return PaymentProcess(sdk, applicationContext).createTinkoffPayPaymentProcess(options, version) } + /** * Запуск экрана Acquiring SDK для привязки новой карты * diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/AsdkState.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/AsdkState.kt index 2453e93d..287115fb 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/AsdkState.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/AsdkState.kt @@ -70,4 +70,12 @@ class BrowseFpsBankState(val paymentId: Long, val deepLink: String, val banks: S * [AcquiringSdk.getState] * @param deepLink диплинк ведущий на форму оплаты, используется для открытия приложения банка */ -class OpenTinkoffPayBankState(val paymentId: Long, val deepLink: String) : AsdkState() \ No newline at end of file +class OpenTinkoffPayBankState(val paymentId: Long, val deepLink: String) : AsdkState() + + +/** + * Состояние открытия приложения, при котором пользователь совершает платеж с помощью яндекс токена + * + * @param yandexToken плажетные данные полученные из яндекса + */ +class YandexPayState(val yandexToken: String) : AsdkState() \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/ViewState.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/ViewState.kt index b419171a..f16ab7c6 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/ViewState.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/ViewState.kt @@ -16,7 +16,6 @@ package ru.tinkoff.acquiring.sdk.models -import ru.tinkoff.acquiring.sdk.responses.Check3dsVersionResponse import ru.tinkoff.acquiring.sdk.threeds.ThreeDsAppBasedTransaction /** diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/FeaturesOptions.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/FeaturesOptions.kt index 77c82e29..84c0e1da 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/FeaturesOptions.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/FeaturesOptions.kt @@ -73,6 +73,11 @@ class FeaturesOptions() : Options(), Parcelable { */ var tinkoffPayEnabled: Boolean = true + /** + * Включение приема платежа через Yandex Pay + */ + var yandexPayEnabled: Boolean = false + /** * Идентификатор карты в системе банка. * Если передан на экран оплаты - в списке карт на экране отобразится первой карта с этим cardId. diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/AttachCardOptions.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/AttachCardOptions.kt index 92dc7eb7..9de88fc4 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/AttachCardOptions.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/AttachCardOptions.kt @@ -51,4 +51,5 @@ class AttachCardOptions : BaseCardsOptions, Parcelable { return arrayOfNulls(size) } } -} \ No newline at end of file +} + diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/BaseAcquiringOptions.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/BaseAcquiringOptions.kt index 0ef30d9f..51f6e2df 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/BaseAcquiringOptions.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/models/options/screen/BaseAcquiringOptions.kt @@ -27,7 +27,7 @@ import ru.tinkoff.acquiring.sdk.models.options.Options * * @author Mariya Chernyadieva */ -open class BaseAcquiringOptions() : Options(), Parcelable { +open class BaseAcquiringOptions() : Options(), Parcelable { lateinit var terminalKey: String private set diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentProcess.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentProcess.kt index 6b5122d3..4c48bd7c 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentProcess.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentProcess.kt @@ -34,6 +34,7 @@ import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions import ru.tinkoff.acquiring.sdk.models.paysources.AttachedCard import ru.tinkoff.acquiring.sdk.models.paysources.CardSource import ru.tinkoff.acquiring.sdk.models.paysources.GooglePay +import ru.tinkoff.acquiring.sdk.models.paysources.YandexPay import ru.tinkoff.acquiring.sdk.models.result.PaymentResult import ru.tinkoff.acquiring.sdk.network.AcquiringApi import ru.tinkoff.acquiring.sdk.network.AcquiringApi.FAIL_MAPI_SESSION_ID @@ -172,6 +173,22 @@ internal constructor( return this } + /** + * Создает объект процесса для проведения оплаты через yandex Pay + * @return сконфигурированный объект для проведения оплаты + */ + fun createYandexPayPaymentProcess(paymentOptions: PaymentOptions, yandexPayToken: String): PaymentProcess { + + this.initRequest = sdk.init { + configure(paymentOptions) + } + this.paymentType = YandexPaymentType + this.paymentSource = YandexPay(yandexPayToken) + + sendToListener(PaymentState.CREATED) + return this + } + /** * Позволяет подписаться на события процесса * @return сконфигурированный объект для проведения оплаты @@ -195,7 +212,7 @@ internal constructor( */ fun start(): PaymentProcess { when (paymentType) { - SbpPaymentType, CardPaymentType, TinkoffPayPaymentType -> callInitRequest(initRequest!!) + SbpPaymentType, CardPaymentType, TinkoffPayPaymentType, YandexPaymentType -> callInitRequest(initRequest!!) FinishPaymentType -> finishPayment(paymentId!!, paymentSource) InitializedSbpPaymentType -> callGetQr(paymentId!!) } @@ -228,6 +245,9 @@ internal constructor( paymentSource is AttachedCard && paymentSource.rebillId != null -> { callChargeRequest(paymentId, paymentSource) } + paymentSource is YandexPay -> { + callFinishAuthorizeRequest(paymentId, paymentSource, email, data = ThreeDsHelper.CollectData.invoke(context, null)) + } paymentSource is GooglePay || state == PaymentState.THREE_DS_V2_REJECTED -> { callFinishAuthorizeRequest(paymentId, paymentSource, email) } @@ -248,7 +268,7 @@ internal constructor( onSuccess = { paymentId = it.paymentId when (paymentType) { - CardPaymentType -> finishPayment(it.paymentId!!, paymentSource, email) + CardPaymentType, YandexPaymentType -> finishPayment(it.paymentId!!, paymentSource, email) SbpPaymentType -> callGetQr(it.paymentId!!) TinkoffPayPaymentType -> callTinkoffPayLinkRequest(it.paymentId!!, tinkoffPayVersion!!) else -> Unit diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentState.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentState.kt index d7947911..f21ca6f2 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentState.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentState.kt @@ -16,6 +16,9 @@ package ru.tinkoff.acquiring.sdk.payment +import ru.tinkoff.acquiring.sdk.models.AsdkState +import ru.tinkoff.acquiring.sdk.models.ThreeDsState + /** * Состояние процесса оплаты * @@ -32,4 +35,22 @@ enum class PaymentState { CHARGE_REJECTED, SUCCESS, ERROR +} + + +/** + * Состояние процесса оплаты для яндекса + * + */ +sealed interface YandexPaymentState { + object Created : YandexPaymentState + object Started : YandexPaymentState + class Registred(val paymentId: Long): YandexPaymentState + + object ThreeDsRejected : YandexPaymentState + class ThreeDsUiNeeded(val asdkState: ThreeDsState) : YandexPaymentState + + class Error(val paymentId: Long?, val throwable: Throwable) : YandexPaymentState + class Success(val paymentId: Long,val cardId: String?, val rebillId: String?) : YandexPaymentState + object Stopped : YandexPaymentState } \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentType.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentType.kt index 6a5c923a..b9d6d0a1 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentType.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/PaymentType.kt @@ -24,5 +24,6 @@ internal sealed class PaymentType internal object SbpPaymentType: PaymentType() internal object InitializedSbpPaymentType: PaymentType() internal object TinkoffPayPaymentType: PaymentType() +internal object YandexPaymentType: PaymentType() internal object CardPaymentType : PaymentType() internal object FinishPaymentType : PaymentType() diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/YandexPaymentProcess.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/YandexPaymentProcess.kt new file mode 100644 index 00000000..0c814eb1 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/YandexPaymentProcess.kt @@ -0,0 +1,159 @@ +package ru.tinkoff.acquiring.sdk.payment + +import android.content.Context +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringApiException +import ru.tinkoff.acquiring.sdk.models.AsdkState +import ru.tinkoff.acquiring.sdk.models.PaymentSource +import ru.tinkoff.acquiring.sdk.models.RejectedState +import ru.tinkoff.acquiring.sdk.models.ThreeDsState +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.models.paysources.YandexPay +import ru.tinkoff.acquiring.sdk.models.result.PaymentResult +import ru.tinkoff.acquiring.sdk.network.AcquiringApi +import ru.tinkoff.acquiring.sdk.payment.PaymentProcess.Companion.configure +import ru.tinkoff.acquiring.sdk.requests.InitRequest +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsAppBasedTransaction +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsDataCollector +import ru.tinkoff.acquiring.sdk.utils.getIpAddress + +/** + * Created by i.golovachev + */ +class YandexPaymentProcess( + private val sdk: AcquiringSdk, + private val context: Context, + private val threeDsDataCollector: ThreeDsDataCollector, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + /** + * Возвращает текущее состояние процесса оплаты + */ + private val _state = MutableStateFlow(null) + val state = _state.asStateFlow() + + private val scope = CoroutineScope( + ioDispatcher + CoroutineExceptionHandler { _, throwable -> handleException(throwable) } + ) + + private lateinit var paymentSource: YandexPay + private var initRequest: InitRequest? = null + private var email: String? = null + + private var paymentResult: PaymentResult? = null + private var sdkState: AsdkState? = null + private var error: Throwable? = null + + private var isChargeWasRejected = false + private var rejectedPaymentId: Long? = null + + fun create(paymentOptions: PaymentOptions, yandexPayToken: String) { + this.initRequest = sdk.init { + configure(paymentOptions) + } + this.paymentSource = YandexPay(yandexPayToken) + } + + /** + * Запускает полный или подтверждающий процесс оплаты в зависимости от созданного процесса + * @return сконфигурированный объект для проведения оплаты + */ + suspend fun start() = scope.launch { + sendToListener(YandexPaymentState.Started) + callInitRequest(initRequest!!) + } + + /** + * Останавливает процесс оплаты + */ + fun stop() { + scope.cancel() + sendToListener(YandexPaymentState.Stopped) + } + + private fun sendToListener(state: YandexPaymentState?) { + this._state.update { state } + } + + private fun handleException(throwable: Throwable) { + if (throwable is AcquiringApiException && throwable.response != null && + throwable.response!!.errorCode == AcquiringApi.API_ERROR_CODE_3DSV2_NOT_SUPPORTED + ) { + sendToListener(YandexPaymentState.ThreeDsRejected) + } else { + error = throwable + val paymentId = (_state.value as? YandexPaymentState.Registred)?.paymentId + sendToListener(YandexPaymentState.Error(paymentId, throwable)) + } + } + + private suspend fun callInitRequest(request: InitRequest) { + if (isChargeWasRejected && rejectedPaymentId != null || sdkState is RejectedState) { + request.data = modifyRejectedData(request) + } + + val initResult = request.performSuspendRequest().getOrThrow() + callFinishAuthorizeRequest( + initResult.paymentId!!, paymentSource, email, + data = threeDsDataCollector.invoke(context,null) + ) + } + + + private fun modifyRejectedData(request: InitRequest): Map { + val map = HashMap() + map[AcquiringApi.RECURRING_TYPE_KEY] = AcquiringApi.RECURRING_TYPE_VALUE + map[AcquiringApi.FAIL_MAPI_SESSION_ID] = rejectedPaymentId?.toString() + ?: (sdkState as? RejectedState)?.rejectedPaymentId.toString() + + val data = request.data?.toMutableMap() ?: mutableMapOf() + data.putAll(map) + + return data.toMap() + } + + private suspend fun callFinishAuthorizeRequest( + paymentId: Long, + paymentSource: PaymentSource, + email: String? = null, + data: Map? = null, + threeDsVersion: String? = null, + threeDsTransaction: ThreeDsAppBasedTransaction? = null + ) { + val ipAddress = if (data != null) getIpAddress() else null + + val finishRequest = sdk.finishAuthorize { + this.paymentId = paymentId + this.email = email + this.paymentSource = paymentSource + this.data = data + ip = ipAddress + sendEmail = email != null + } + + val response = finishRequest.performSuspendRequest().getOrThrow() + val threeDsData = response.getThreeDsData(threeDsVersion) + + if (threeDsData.isThreeDsNeed) { + ThreeDsState(threeDsData, threeDsTransaction).also { + sdkState = it + sendToListener(YandexPaymentState.ThreeDsUiNeeded(it)) + } + } else { + paymentResult = PaymentResult(response.paymentId, null, response.rebillId) + sendToListener( + YandexPaymentState.Success( + response.paymentId!!, + null, + response.rebillId + ) + ) + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/DialogActivityExt.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/DialogActivityExt.kt new file mode 100644 index 00000000..8e3e6c1a --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/DialogActivityExt.kt @@ -0,0 +1,23 @@ +package ru.tinkoff.acquiring.sdk.redesign.dialog + +import androidx.fragment.app.FragmentActivity +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * Created by i.golovachev + */ + +internal fun FragmentActivity.dismissDialog(tag: String = DIALOG_ACTIVITY_EXT) { + val f = supportFragmentManager.findFragmentByTag(tag) + if (f == null) { + return + } else { + (f as BottomSheetDialogFragment).dismissAllowingStateLoss() + } +} + +internal fun FragmentActivity.showDialog(dialogFragment: BottomSheetDialogFragment) { + dialogFragment.show(supportFragmentManager, DIALOG_ACTIVITY_EXT) +} + +private const val DIALOG_ACTIVITY_EXT = "DIALOG_ACTIVITY_EXT" \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentFailureDialogFragment.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentFailureDialogFragment.kt new file mode 100644 index 00000000..7bc9a942 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentFailureDialogFragment.kt @@ -0,0 +1,36 @@ +package ru.tinkoff.acquiring.sdk.redesign.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.ui.customview.LoaderButton +import ru.tinkoff.acquiring.sdk.utils.lazyView + +internal class PaymentFailureDialogFragment : BottomSheetDialogFragment() { + + private val buttonChooseAnotherMethod: LoaderButton + by lazyView(R.id.acq_button_choose_another_method) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, theme) + } + + var onChooseAnotherMethod: OnChooseAnotherMethod? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.acq_fragment_payment_failure, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + buttonChooseAnotherMethod.setOnClickListener { onChooseAnotherMethod?.invoke(this) } + } + + fun interface OnChooseAnotherMethod { + operator fun invoke(fragment: PaymentFailureDialogFragment) + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentLCEDialogFragment.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentLCEDialogFragment.kt new file mode 100644 index 00000000..b5a3d049 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentLCEDialogFragment.kt @@ -0,0 +1,90 @@ +package ru.tinkoff.acquiring.sdk.redesign.dialog + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ViewFlipper +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.ui.customview.LoaderButton +import ru.tinkoff.acquiring.sdk.utils.lazyUnsafe +import ru.tinkoff.acquiring.sdk.utils.lazyView + +internal class PaymentLCEDialogFragment : BottomSheetDialogFragment() { + + private val isCancelableInternal: Boolean by lazyUnsafe { + arguments?.getBoolean(PaymentLCEDialogFragment::isCancelableInternal.name) ?: false + } + private val viewFlipper: ViewFlipper + by lazyView(R.id.acq_fragment_payment_lce) + private val buttonChooseAnotherMethod: LoaderButton + by lazyView(R.id.acq_button_choose_another_method) + private val buttonOk: LoaderButton by lazyView(R.id.acq_button_ok) + + private var onDismissInternal: (() -> Unit)? = null + + var onViewCreated: OnViewCreated? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, theme) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = + inflater.inflate(R.layout.acq_fragment_payment_lce, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialog?.window?.attributes?.windowAnimations = R.style.AcqBottomSheetAnim + isCancelable = isCancelableInternal + onViewCreated?.invoke(this) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismissInternal?.invoke() + } + + fun loading() { + viewFlipper.displayedChild = 0 + isCancelable = isCancelableInternal + } + + fun failure(onChooseAnother: () -> Unit) { + viewFlipper.displayedChild = 1 + buttonChooseAnotherMethod.setOnClickListener { + onChooseAnother() + } + isCancelable = true + onDismissInternal = onChooseAnother + } + + fun success(onOkClick: () -> Unit) { + viewFlipper.displayedChild = 2 + buttonOk.setOnClickListener { onOkClick() } + isCancelable = true + onDismissInternal = onOkClick + } + + companion object { + + fun create(isCancelable: Boolean): PaymentLCEDialogFragment { + val f = PaymentLCEDialogFragment() + val arg = Bundle().apply { + putBoolean(PaymentLCEDialogFragment::isCancelableInternal.name, isCancelable) + } + f.arguments = arg + return f + } + } + + fun interface OnViewCreated { + operator fun invoke(f: PaymentLCEDialogFragment) + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentProgressDialogFragment.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentProgressDialogFragment.kt new file mode 100644 index 00000000..423b7931 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentProgressDialogFragment.kt @@ -0,0 +1,22 @@ +package ru.tinkoff.acquiring.sdk.redesign.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.tinkoff.acquiring.sdk.R + +internal class PaymentProgressDialogFragment : BottomSheetDialogFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, theme) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.acq_fragment_payment_progress, container, false) + +} + + diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentSuccessDialogFragment.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentSuccessDialogFragment.kt new file mode 100644 index 00000000..69477562 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/redesign/dialog/PaymentSuccessDialogFragment.kt @@ -0,0 +1,37 @@ +package ru.tinkoff.acquiring.sdk.redesign.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.models.result.AsdkResult +import ru.tinkoff.acquiring.sdk.ui.customview.LoaderButton +import ru.tinkoff.acquiring.sdk.utils.lazyView + +internal class PaymentSuccessDialogFragment : BottomSheetDialogFragment() { + + private val buttonOk: LoaderButton by lazyView(R.id.acq_button_ok) + + var paymentSuccessClick: OnPaymentSuccessClick? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, theme) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflater.inflate(R.layout.acq_fragment_payment_success, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + buttonOk.setOnClickListener { paymentSuccessClick?.invoke(this) } + } + + + fun interface OnPaymentSuccessClick { + operator fun invoke(fragment: PaymentSuccessDialogFragment) + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/threeds/ThreeDsHelper.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/threeds/ThreeDsHelper.kt index c86f89dd..0df6c16a 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/threeds/ThreeDsHelper.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/threeds/ThreeDsHelper.kt @@ -153,7 +153,7 @@ object ThreeDsHelper { else -> CERTS_CONFIG_URL_PROD } - object CollectData { + object CollectData : ThreeDsDataCollector { private const val THREE_DS_CALLED_FLAG = "Y" private const val THREE_DS_NOT_CALLED_FLAG = "N" @@ -161,9 +161,9 @@ object ThreeDsHelper { private val NOTIFICATION_URL = "${AcquiringApi.getUrl(COMPLETE_3DS_METHOD_V2)}/$COMPLETE_3DS_METHOD_V2" private val TERM_URL_V2 = ThreeDsActivity.TERM_URL_V2 - operator fun invoke(context: Context, response: Check3dsVersionResponse): MutableMap { + override operator fun invoke(context: Context, response: Check3dsVersionResponse?): MutableMap { var threeDSCompInd = THREE_DS_NOT_CALLED_FLAG - if (response.threeDsMethodUrl != null) { + if (response?.threeDsMethodUrl != null) { val hiddenWebView = WebView(context) val threeDsMethodData = JSONObject().apply { @@ -407,4 +407,9 @@ object ThreeDsHelper { class ThreeDsAppBasedTransaction( val wrapper: ThreeDSWrapper, val transaction: Transaction -) \ No newline at end of file +) + +interface ThreeDsDataCollector { + + operator fun invoke(context: Context, response: Check3dsVersionResponse?): MutableMap +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/ThreeDsActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/ThreeDsActivity.kt index a588604a..b2e5d264 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/ThreeDsActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/ThreeDsActivity.kt @@ -159,13 +159,6 @@ internal class ThreeDsActivity : BaseAcquiringActivity() { private var canceled = false - override fun shouldInterceptRequest( - view: WebView?, - request: WebResourceRequest? - ): WebResourceResponse? { - return provideThreeDsSubmitV2Delegate().shouldInterceptRequest(request, data) - } - override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/YandexPaymentActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/YandexPaymentActivity.kt new file mode 100644 index 00000000..e85d6738 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/YandexPaymentActivity.kt @@ -0,0 +1,155 @@ +/* + * 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.ui.activities + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import ru.tinkoff.acquiring.sdk.models.* +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.models.result.AsdkResult +import ru.tinkoff.acquiring.sdk.redesign.dialog.* +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper +import ru.tinkoff.acquiring.sdk.viewmodel.YandexPaymentViewModel + +/** + * @author Ivan Golovachev + */ +internal class YandexPaymentActivity : TransparentActivity() { + + private lateinit var paymentViewModel: YandexPaymentViewModel + private lateinit var paymentOptions: PaymentOptions + private var asdkState: AsdkState = DefaultState + private var paymentLCEDialogFragment: PaymentLCEDialogFragment = + PaymentLCEDialogFragment.create(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + paymentOptions = options as PaymentOptions + asdkState = paymentOptions.asdkState + + initViews() + bottomContainer.isVisible = false + + paymentViewModel = + provideViewModel(YandexPaymentViewModel::class.java) as YandexPaymentViewModel + observeLiveData() + + if (savedInstanceState == null) { + (asdkState as? YandexPayState)?.let { + paymentViewModel.startYandexPayPayment(paymentOptions, it.yandexToken) + } + } + + paymentViewModel.checkoutAsdkState(asdkState) + } + + override fun handleLoadState(loadState: LoadState) { + super.handleLoadState(loadState) + when (loadState) { + is LoadingState -> { + getStateDialog { it.loading() } + } + } + } + + private fun observeLiveData() { + with(paymentViewModel) { + loadStateLiveData.observe(this@YandexPaymentActivity, Observer { handleLoadState(it) }) + screenStateLiveData.observe(this@YandexPaymentActivity, Observer { handleScreenState(it) }) + screenChangeEventLiveData.observe(this@YandexPaymentActivity, Observer { handleScreenChangeEvent(it) }) + paymentResultLiveData.observe(this@YandexPaymentActivity, Observer { + getStateDialog { f -> + f.success { finishWithSuccess(it) } + } + } + ) + } + } + + private fun handleScreenChangeEvent(screenChangeEvent: SingleEvent) { + screenChangeEvent.getValueIfNotHandled()?.let { screen -> + when (screen) { + is ThreeDsScreenState -> try { + ThreeDsHelper.Launch.launchBrowserBased( + this@YandexPaymentActivity, + THREE_DS_REQUEST_CODE, + options, + screen.data, + ) + } catch (e: Throwable) { + getStateDialog { it.failure { finishWithError(e) } } + } + else -> Unit + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + paymentViewModel.onDismissDialog() + if (requestCode == THREE_DS_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK && data != null) { + getStateDialog { + it.success { + finishWithSuccess(data.getSerializableExtra(ThreeDsHelper.Launch.RESULT_DATA) as AsdkResult) + } + } + } else if (resultCode == ThreeDsHelper.Launch.RESULT_ERROR) { + getStateDialog { + it.failure { + finishWithError(data?.getSerializableExtra(ThreeDsHelper.Launch.ERROR_DATA) as Throwable) + } + } + } else { + setResult(Activity.RESULT_CANCELED) + finish() + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun handleScreenState(screenState: ScreenState) { + when (screenState) { + is FinishWithErrorScreenState -> getStateDialog { + it.failure { + paymentViewModel.onDismissDialog() + finishWithError(screenState.error) + } + } + is ErrorScreenState -> getStateDialog { + it.failure { + paymentViewModel.onDismissDialog() + finishWithError(IllegalStateException(screenState.message)) + } + } + else -> Unit + } + } + + private fun getStateDialog(block: PaymentLCEDialogFragment.OnViewCreated? = null) { + if (paymentLCEDialogFragment.isAdded.not()) { + paymentLCEDialogFragment.onViewCreated = block + showDialog(paymentLCEDialogFragment) + } else { + block?.invoke(paymentLCEDialogFragment) + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/customview/LoaderButton.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/customview/LoaderButton.kt new file mode 100644 index 00000000..32c4d523 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/customview/LoaderButton.kt @@ -0,0 +1,79 @@ +package ru.tinkoff.acquiring.sdk.ui.customview + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.core.content.withStyledAttributes +import androidx.core.view.isGone +import androidx.core.view.isVisible +import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.utils.dpToPx + +internal class LoaderButton +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = R.style.AcqLoaderButton +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + var text: CharSequence + get() = textView.text + set(value) { + textView.text = value + } + + var isLoading = false + set(value) { + field = value + textView.isGone = field + progressBar.isVisible = field + } + + val textView = TextView(context).apply { + textSize = 16f + setTextColor(ResourcesCompat.getColorStateList( + context.resources, R.color.acq_button_text_selector, context.theme)) + } + var textColor: ColorStateList? + get() = textView.textColors + set(value) { + textView.setTextColor(value) + } + + val progressBar = ProgressBar(context).apply { + isIndeterminate = true + indeterminateTintList = ColorStateList.valueOf(ResourcesCompat.getColor( + context.resources, R.color.acq_colorButtonText, context.theme)) + isGone = true + } + var progressColor: ColorStateList? + get() = progressBar.indeterminateTintList + set(value) { + progressBar.indeterminateTintList = value + } + + init { + addView(textView, LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + gravity = Gravity.CENTER + }) + addView(progressBar, LayoutParams(context.dpToPx(24), context.dpToPx(24)).apply { + gravity = Gravity.CENTER + }) + + context.withStyledAttributes(attrs, R.styleable.LoaderButton, defStyleAttr, defStyleRes) { + text = getString(R.styleable.LoaderButton_acq_text).orEmpty() + textColor = getColorStateList(R.styleable.LoaderButton_acq_text_color) + getDrawable(R.styleable.LoaderButton_acq_background)?.let { background = it } + ?: setBackgroundResource(R.drawable.acq_button_yellow_bg) + progressColor = getColorStateList(R.styleable.LoaderButton_acq_progress_color) + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/fragments/YandexPaymentStubFragment.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/fragments/YandexPaymentStubFragment.kt new file mode 100644 index 00000000..68b1a0dd --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/fragments/YandexPaymentStubFragment.kt @@ -0,0 +1,19 @@ +package ru.tinkoff.acquiring.sdk.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import ru.tinkoff.acquiring.sdk.R + +/** + * Created by i.golovachev + */ +internal class YandexPaymentStubFragment : BaseAcquiringFragment() { + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.acq_fragment_yandex_stub, container, false) + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/Money.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/Money.kt index f0d7b930..4aa9dff7 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/Money.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/Money.kt @@ -99,7 +99,7 @@ class Money private constructor(val coins: Long) : Serializable, Comparable Unit) { + for (i in 0 until childCount) { + action(getChildAt(i)) + } +} + +internal fun lerp(start: Int, end: Int, fraction: Float): Int { + return (start + (end - start) * fraction).roundToInt() +} + +internal fun lerp(start: Long, end: Long, fraction: Float): Long { + return (start + (end - start) * fraction).roundToLong() +} + +internal fun lerp(start: Float, end: Float, fraction: Float): Float { + return (start + (end - start) * fraction) +} + +internal fun lazyUnsafe(initializer: () -> T): Lazy = + lazy(LazyThreadSafetyMode.NONE, initializer) + +internal fun Fragment.lazyView(@IdRes id: Int): Lazy = + lazyUnsafe { requireView().findViewById(id) } + +internal fun Activity.lazyView(@IdRes id: Int): Lazy = + lazyUnsafe { findViewById(id) } \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/AttachCardViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/AttachCardViewModel.kt index af5db6fc..4fbfcf47 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/AttachCardViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/AttachCardViewModel.kt @@ -36,6 +36,7 @@ import ru.tinkoff.acquiring.sdk.network.AcquiringApi import ru.tinkoff.acquiring.sdk.responses.AttachCardResponse import ru.tinkoff.acquiring.sdk.responses.Check3dsVersionResponse import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper +import kotlin.properties.ReadWriteProperty /** * @author Mariya Chernyadieva diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/PaymentViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/PaymentViewModel.kt index d018b0aa..f5b16d76 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/PaymentViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/PaymentViewModel.kt @@ -167,6 +167,11 @@ internal class PaymentViewModel( paymentProcess.createTinkoffPayPaymentProcess(paymentOptions, tinkoffPayVersion).subscribe(paymentListener).start() } + fun startYandexPayPayment(paymentOptions: PaymentOptions, yandexPayToken: String) { + changeScreenState(LoadingState) + paymentProcess.createYandexPayPaymentProcess(paymentOptions, yandexPayToken).subscribe(paymentListener).start() + } + fun finishPayment(paymentId: Long, paymentSource: PaymentSource, email: String? = null) { changeScreenState(LoadingState) paymentProcess.createFinishProcess(paymentId, paymentSource, email).subscribe(paymentListener).start() diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ViewModelProviderFactory.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ViewModelProviderFactory.kt index 1e6292d2..c3ad2863 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ViewModelProviderFactory.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ViewModelProviderFactory.kt @@ -35,10 +35,12 @@ internal class ViewModelProviderFactory( QrViewModel::class.java to QrViewModel(application, handleErrorsInSdk, sdk), ThreeDsViewModel::class.java to ThreeDsViewModel(application, handleErrorsInSdk, sdk), SavedCardsViewModel::class.java to SavedCardsViewModel(application, handleErrorsInSdk, sdk), - NotificationPaymentViewModel::class.java to NotificationPaymentViewModel(application, handleErrorsInSdk, sdk)) + NotificationPaymentViewModel::class.java to NotificationPaymentViewModel(application, handleErrorsInSdk, sdk), + YandexPaymentViewModel::class.java to YandexPaymentViewModel(application, handleErrorsInSdk, sdk) + ) @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return viewModelCollection[modelClass] as T + return (viewModelCollection)[modelClass] as T } } \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/YandexPaymentViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/YandexPaymentViewModel.kt new file mode 100644 index 00000000..6d0fb373 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/YandexPaymentViewModel.kt @@ -0,0 +1,89 @@ +package ru.tinkoff.acquiring.sdk.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.models.* +import ru.tinkoff.acquiring.sdk.models.LoadingState +import ru.tinkoff.acquiring.sdk.models.PaymentScreenState +import ru.tinkoff.acquiring.sdk.models.ThreeDsScreenState +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.models.result.PaymentResult +import ru.tinkoff.acquiring.sdk.payment.* +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper + +/** + * Created by i.golovachev + */ +internal class YandexPaymentViewModel( + application: Application, + handleErrorsInSdk: Boolean, + sdk: AcquiringSdk +) : BaseAcquiringViewModel(application, handleErrorsInSdk, sdk) { + + private val paymentProcess: YandexPaymentProcess = YandexPaymentProcess(sdk, context, ThreeDsHelper.CollectData) + private val paymentResult: MutableLiveData = MutableLiveData() + val paymentResultLiveData: LiveData = paymentResult + + fun startYandexPayPayment(paymentOptions: PaymentOptions, yandexPayToken: String) { + changeScreenState(LoadingState) + + viewModelScope.launch { + paymentProcess.create(paymentOptions, yandexPayToken) + paymentProcess.start() + } + + viewModelScope.launch { + paymentProcess.state.launchAndCollect() + } + } + + fun checkoutAsdkState(state: AsdkState) { + when (state) { + is ThreeDsState -> changeScreenState(ThreeDsScreenState(state.data, state.transaction)) + else -> changeScreenState(PaymentScreenState) + } + } + + fun onDismissDialog() { + paymentProcess.stop() + } + + private fun StateFlow.launchAndCollect() { + viewModelScope.launch { + buffer(0, BufferOverflow.DROP_OLDEST) + .collectLatest { + when (it) { + is YandexPaymentState.ThreeDsUiNeeded -> { + changeScreenState( + ThreeDsScreenState( + it.asdkState.data, + it.asdkState.transaction + ) + ) + coroutine.runWithDelay(500) { + changeScreenState(LoadedState) + } + } + is YandexPaymentState.Success -> { + changeScreenState(LoadedState) + paymentResult.value = + PaymentResult(it.paymentId, it.cardId, it.rebillId) + } + is YandexPaymentState.Error -> { + changeScreenState(LoadedState) + handleException(it.throwable) + } + else -> Unit + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/res/anim/acq_slide_down.xml b/ui/src/main/res/anim/acq_slide_down.xml new file mode 100644 index 00000000..c600e92e --- /dev/null +++ b/ui/src/main/res/anim/acq_slide_down.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/anim/acq_slide_up.xml b/ui/src/main/res/anim/acq_slide_up.xml new file mode 100644 index 00000000..7f1deff6 --- /dev/null +++ b/ui/src/main/res/anim/acq_slide_up.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/ui/src/main/res/color/acq_button_text_selector.xml b/ui/src/main/res/color/acq_button_text_selector.xml new file mode 100644 index 00000000..88aa77ed --- /dev/null +++ b/ui/src/main/res/color/acq_button_text_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/color/acq_secondary_button_text_selector.xml b/ui/src/main/res/color/acq_secondary_button_text_selector.xml new file mode 100644 index 00000000..17a6c88c --- /dev/null +++ b/ui/src/main/res/color/acq_secondary_button_text_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/drawable/acq_bottom_sheet_bg.xml b/ui/src/main/res/drawable/acq_bottom_sheet_bg.xml new file mode 100644 index 00000000..8626ac8d --- /dev/null +++ b/ui/src/main/res/drawable/acq_bottom_sheet_bg.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/drawable/acq_button_flat_bg.xml b/ui/src/main/res/drawable/acq_button_flat_bg.xml new file mode 100644 index 00000000..4a7587a7 --- /dev/null +++ b/ui/src/main/res/drawable/acq_button_flat_bg.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/drawable/acq_button_secondary_bg.xml b/ui/src/main/res/drawable/acq_button_secondary_bg.xml new file mode 100644 index 00000000..c2137943 --- /dev/null +++ b/ui/src/main/res/drawable/acq_button_secondary_bg.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/drawable/acq_button_yellow_bg.xml b/ui/src/main/res/drawable/acq_button_yellow_bg.xml new file mode 100644 index 00000000..65f17bc6 --- /dev/null +++ b/ui/src/main/res/drawable/acq_button_yellow_bg.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/drawable/acq_ic_check_circle_positive.xml b/ui/src/main/res/drawable/acq_ic_check_circle_positive.xml new file mode 100644 index 00000000..eac6b95a --- /dev/null +++ b/ui/src/main/res/drawable/acq_ic_check_circle_positive.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/ui/src/main/res/drawable/acq_ic_cross_circle.xml b/ui/src/main/res/drawable/acq_ic_cross_circle.xml new file mode 100644 index 00000000..2eb64a89 --- /dev/null +++ b/ui/src/main/res/drawable/acq_ic_cross_circle.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/ui/src/main/res/layout/acq_fragment_payment_failure.xml b/ui/src/main/res/layout/acq_fragment_payment_failure.xml new file mode 100644 index 00000000..5ae36f09 --- /dev/null +++ b/ui/src/main/res/layout/acq_fragment_payment_failure.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/acq_fragment_payment_lce.xml b/ui/src/main/res/layout/acq_fragment_payment_lce.xml new file mode 100644 index 00000000..20f319db --- /dev/null +++ b/ui/src/main/res/layout/acq_fragment_payment_lce.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/acq_fragment_payment_progress.xml b/ui/src/main/res/layout/acq_fragment_payment_progress.xml new file mode 100644 index 00000000..0c97975f --- /dev/null +++ b/ui/src/main/res/layout/acq_fragment_payment_progress.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/acq_fragment_payment_success.xml b/ui/src/main/res/layout/acq_fragment_payment_success.xml new file mode 100644 index 00000000..5bb389d7 --- /dev/null +++ b/ui/src/main/res/layout/acq_fragment_payment_success.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/layout/acq_fragment_yandex_stub.xml b/ui/src/main/res/layout/acq_fragment_yandex_stub.xml new file mode 100644 index 00000000..3e647a8d --- /dev/null +++ b/ui/src/main/res/layout/acq_fragment_yandex_stub.xml @@ -0,0 +1,31 @@ + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/values-night/colors.xml b/ui/src/main/res/values-night/colors.xml index 1fbfbb43..cb81b589 100644 --- a/ui/src/main/res/values-night/colors.xml +++ b/ui/src/main/res/values-night/colors.xml @@ -18,6 +18,7 @@ #1C1C1E #333333 + #0FFFFFFF @android:color/transparent @color/acq_colorMainDark #FFFFFF @@ -25,8 +26,14 @@ #F6F7F8 @color/acq_colorTitle - #CF6679 + #4dffffff #727272 @color/acq_colorMain + #4DFFFFFF + + #1affffff + #26ffffff + + #333335 \ No newline at end of file diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml index ea027a84..040267f1 100644 --- a/ui/src/main/res/values-ru/strings.xml +++ b/ui/src/main/res/values-ru/strings.xml @@ -24,4 +24,11 @@ Выберите приложение + Обрабатываем платеж + Это займет некоторое время + Оплачено + Не получилось оплатить + Понятно + Воспользуйтесь другим\nспособом оплаты + \ No newline at end of file diff --git a/ui/src/main/res/values/attrs.xml b/ui/src/main/res/values/attrs.xml index a38aa96a..ab6a301c 100644 --- a/ui/src/main/res/values/attrs.xml +++ b/ui/src/main/res/values/attrs.xml @@ -60,4 +60,11 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/main/res/values/colors.xml b/ui/src/main/res/values/colors.xml index 68240d0f..a1471ab5 100644 --- a/ui/src/main/res/values/colors.xml +++ b/ui/src/main/res/values/colors.xml @@ -21,10 +21,14 @@ #428BF9 #3e4757 #FFDD2D - #55ffdd2d - #F6F7F8 + #FFCD33 + #08001024 + @color/acq_colorButtonDisable + #08001024 #000000 #333333 + #ff9299a2 + #38001024 #C7C9CC #9E9E9E #4D000000 @@ -46,4 +50,15 @@ #FFFFFF #2CFFFFFF @color/acq_colorBrandGray + + #f7f7f7 + + #08001024 + #0f001024 + + #333333 + #333333 + #38001024 + #FFDD2D + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index e09d5127..1e0f1bf1 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -26,4 +26,11 @@ fps Tinkoff + Processing the payment + it will take some time + Paid + Payment error + OK + Use a different payment method + diff --git a/ui/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml index 627f8d40..27ce66b9 100644 --- a/ui/src/main/res/values/styles.xml +++ b/ui/src/main/res/values/styles.xml @@ -216,4 +216,33 @@ ?colorAccent + + + + + + + + + + diff --git a/ui/src/test/java/yandex/YandexPaymentProcessEnv.kt b/ui/src/test/java/yandex/YandexPaymentProcessEnv.kt new file mode 100644 index 00000000..45544fa4 --- /dev/null +++ b/ui/src/test/java/yandex/YandexPaymentProcessEnv.kt @@ -0,0 +1,103 @@ +package yandex + +import android.content.Context +import kotlinx.coroutines.* +import org.mockito.kotlin.* +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.payment.YandexPaymentProcess +import ru.tinkoff.acquiring.sdk.requests.FinishAuthorizeRequest +import ru.tinkoff.acquiring.sdk.requests.InitRequest +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest +import ru.tinkoff.acquiring.sdk.responses.FinishAuthorizeResponse +import ru.tinkoff.acquiring.sdk.responses.InitResponse +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsDataCollector +import ru.tinkoff.acquiring.sdk.ui.activities.ThreeDsActivity + +private val yandexPay3dsDataMap = mapOf( + "threeDSCompInd" to "Y", + "language" to "ru-RU", + "timezone" to "", + "screen_height" to "120", + "screen_width" to "120", + "cresCallbackUrl" to ThreeDsActivity.TERM_URL_V2 +).toMutableMap() + +class YandexPaymentProcessEnv( + // const + val yandexToken: String? = "yandexToken", + val paymentId: Long = 1, + val paReq: String = "paReq", + val md: String = "md", + val tdsServerTransId: String = "tdsServerTransId", + val acsTransId : String= "acsTransId", + + // env + val ioDispatcher: CoroutineDispatcher = Dispatchers.Unconfined, + + // mocks + val initRequestMock: InitRequest = mock(), + val faRequestMock: FinishAuthorizeRequest = mock(), + val sdk: AcquiringSdk = mock(), + val context: Context = mock(), + val threeDsDataCollector: ThreeDsDataCollector = mock { on { invoke(any(), any()) } doReturn yandexPay3dsDataMap }, + val process: YandexPaymentProcess = YandexPaymentProcess(sdk, context, threeDsDataCollector, ioDispatcher) +) { + + init { + whenever(sdk.init(any())).doReturn(initRequestMock) + whenever(sdk.finishAuthorize(any())).doReturn(faRequestMock) + } + + fun shutdownMock() { + reset(initRequestMock) + reset(context) + } + + suspend fun setInitResult(response: InitResponse) { + val result = Result.success(response) + + whenever(initRequestMock.performSuspendRequest()) + .doReturn(result) + } + + suspend fun setInitResult(throwable: Throwable) { + val result = Result.failure(throwable) + + whenever(initRequestMock.performSuspendRequest()) + .doReturn(result) + } + + suspend fun setInitResult(definePaymentId: Long? = null) { + val response = InitResponse(paymentId = definePaymentId ?: paymentId) + val result = Result.success(response) + + whenever(initRequestMock.performSuspendRequest()) + .doReturn(result) + } + + suspend fun setFAResult(response : FinishAuthorizeResponse = FinishAuthorizeResponse()) { + val result = Result.success(response) + + whenever(faRequestMock.performSuspendRequest()) + .doReturn(result) + } + + suspend fun setFAResult(throwable: Throwable) { + val result = Result.failure(throwable) + + whenever(faRequestMock.performSuspendRequest()) + .doReturn(result) + } +} + +internal fun YandexPaymentProcessEnv.runWithEnv( + given: suspend YandexPaymentProcessEnv.() -> Unit, + `when`: suspend YandexPaymentProcessEnv.() -> Unit, + then: suspend YandexPaymentProcessEnv.() -> Unit +) { + runBlocking { + launch { given.invoke(this@runWithEnv) }.join() + launch { `when`.invoke(this@runWithEnv) }.join() + launch { then.invoke(this@runWithEnv) }.join() + } +} \ No newline at end of file diff --git a/ui/src/test/java/yandex/YandexProcessPaymentTest.kt b/ui/src/test/java/yandex/YandexProcessPaymentTest.kt new file mode 100644 index 00000000..60a66a10 --- /dev/null +++ b/ui/src/test/java/yandex/YandexProcessPaymentTest.kt @@ -0,0 +1,175 @@ +package yandex + +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito.times +import org.mockito.kotlin.verify +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.payment.YandexPaymentState +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest +import ru.tinkoff.acquiring.sdk.responses.FinishAuthorizeResponse + +/** + * Created by i.golovachev + */ +class YandexProcessPaymentTest { + + private val processEnv = YandexPaymentProcessEnv() + + @Test + //#2354687 + fun `When Init complete Then FA called`() = processEnv.runWithEnv( + given = { + setInitResult() + setFAResult() + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + verify(processEnv.faRequestMock, times(1)).performSuspendRequest() + } + ) + + @Test + //#2354729 + fun `When FA complete and return paReq Then 3dsv1 redirected`() = processEnv.runWithEnv( + given = { + setInitResult() + setFAResult( + FinishAuthorizeResponse( + paReq = paReq, + md = md, + paymentId = paymentId, + status = ResponseStatus.THREE_DS_CHECKING + ) + ) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + val asdkState = (value as YandexPaymentState.ThreeDsUiNeeded).asdkState + Assert.assertFalse( + asdkState.data.is3DsVersion2 + ) + } + ) + + @Test + //#2354750 + fun `When FA complete and return TdsServerTransId Then 3dsv2 redirected`() = + processEnv.runWithEnv( + given = { + setInitResult(1L) + setFAResult( + FinishAuthorizeResponse( + tdsServerTransId = tdsServerTransId, + acsTransId = acsTransId, + paymentId = paymentId, + status = ResponseStatus.THREE_DS_CHECKING + ) + ) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + val asdkState = (value as YandexPaymentState.ThreeDsUiNeeded).asdkState + Assert.assertTrue( + asdkState.data.is3DsVersion2 + ) + } + ) + + @Test + //#2354714 + fun `When FA throw error Then give error state`() = processEnv.runWithEnv( + given = { + setInitResult() + setFAResult(IllegalStateException()) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + Assert.assertTrue( + value is YandexPaymentState.Error + ) + } + ) + + @Test + //#2354688 + fun `When Init throw error Then give error state`() = processEnv.runWithEnv( + given = { + setInitResult(IllegalStateException()) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + Assert.assertTrue( + value is YandexPaymentState.Error + ) + } + ) + + @Test + //#2354715 + fun `When FA return CONFIRMED Then give success state`() = processEnv.runWithEnv( + given = { + setInitResult() + setFAResult( + FinishAuthorizeResponse( + paymentId = paymentId, + status = ResponseStatus.CONFIRMED + ) + ) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + Assert.assertTrue( + value is YandexPaymentState.Success + ) + } + ) + + @Test + //#2354715 + fun `When FA return AUTHORIZED Then give success state`() = processEnv.runWithEnv( + given = { + setInitResult() + setFAResult( + FinishAuthorizeResponse( + paymentId = paymentId, + status = ResponseStatus.AUTHORIZED + ) + ) + }, + `when` = { + process.create(PaymentOptions(), yandexToken!!) + process.start().join() + }, + then = { + val value = process.state.value + Assert.assertTrue( + value is YandexPaymentState.Success + ) + } + ) +} \ No newline at end of file diff --git a/yandexpay/.gitignore b/yandexpay/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/yandexpay/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/yandexpay/build.gradle b/yandexpay/build.gradle new file mode 100644 index 00000000..fefcd703 --- /dev/null +++ b/yandexpay/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply from: '../gradle/dokka.gradle' +apply from: rootProject.file('gradle/versions.gradle') +apply from: '../gradle/publish.gradle' + +android { + compileSdkVersion rootProject.compileSdk + buildToolsVersion rootProject.buildTools + + defaultConfig { + minSdkVersion rootProject.yandexPayMinSdk + targetSdkVersion rootProject.targetSdk + versionCode Integer.parseInt(VERSION_CODE) + versionName VERSION_NAME + buildConfigField("String", "ASDK_VERSION_NAME", "\"$VERSION_NAME\"") + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + tasks.whenTaskAdded { task -> + if (task.name.contains("AndroidTest")) { + task.enabled = false + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "androidx.appcompat:appcompat:$appCompatVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleExtensionsVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + + api project(':core') + implementation project(':ui') + + implementation "androidx.appcompat:appcompat:${appCompatVersion}" + implementation("com.yandex.pay:core:${yandexPayVersion}") + + + testImplementation 'junit:junit:4.13' + testImplementation "org.mockito.kotlin:mockito-kotlin:${mokitoKotlin}" + testImplementation 'org.mockito:mockito-inline:2.13.0' +} diff --git a/yandexpay/gradle.properties b/yandexpay/gradle.properties new file mode 100644 index 00000000..6338cf13 --- /dev/null +++ b/yandexpay/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=yandexpay +POM_ARTIFACT_ID=yandexpay +POM_PACKAGING=aar \ No newline at end of file diff --git a/yandexpay/src/main/AndroidManifest.xml b/yandexpay/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f4e448d7 --- /dev/null +++ b/yandexpay/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/TinkoffAcquiringYandexExt.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/TinkoffAcquiringYandexExt.kt new file mode 100644 index 00000000..57dcb30a --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/TinkoffAcquiringYandexExt.kt @@ -0,0 +1,78 @@ +package ru.tinkoff.acquiring.yandexpay + +import androidx.fragment.app.FragmentActivity +import com.yandex.pay.core.OpenYandexPayContract +import com.yandex.pay.core.YandexPayResult +import ru.tinkoff.acquiring.sdk.TinkoffAcquiring +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.yandexpay.models.YandexPayData + +/** + * Created by i.golovachev + */ +internal typealias AcqYandexPayCallback = (AcqYandexPayResult) -> Unit +typealias AcqYandexPayErrorCallback = (Throwable) -> Unit +typealias AcqYandexPayCancelCallback = () -> Unit + + +/** + * Создает обертку для элемента yandex-pay-button для выбора средства оплаты + * + * @param activity контекст для дальнешей навигации платежного флоу из Activity + * @param yandexPayData параметры, для настройки yandex-pay библиотеки, полученные от бэка + * @param options настройки платежной сессии + * @param yandexPayRequestCode код для получения результата, по завершению работы экрана Acquiring SDK + * @param isProd выбор окружения для яндекса YandexPayEnvironment.Prod или YandexPayEnvironment.Sandbox + * @param enableLogging включение логгирования событий YandexPay + * @param themeId идентификатор темы приложения, параметры которого будет использованы для + * отображение yandex-pay-button + * @param onYandexErrorCallback дополнительный метод для возможности обработки ошибки от яндекса на + * стороне клиентского приложения + * @param onYandexCancelCallback дополнительный метод для возможности обработки отмены + */ +fun TinkoffAcquiring.createYandexPayButtonFragment( + activity: FragmentActivity, + yandexPayData: YandexPayData, + options: PaymentOptions, + yandexPayRequestCode: Int, + isProd: Boolean = false, + enableLogging: Boolean = false, + themeId: Int? = null, + onYandexErrorCallback: AcqYandexPayErrorCallback? = null, + onYandexCancelCallback: AcqYandexPayCancelCallback? = null +): YandexButtonFragment { + val fragment = YandexButtonFragment.newInstance(yandexPayData, options, isProd, enableLogging, themeId) + addYandexResultListener(fragment, activity, yandexPayRequestCode, onYandexErrorCallback, onYandexCancelCallback) + return fragment +} + +/** + * Создает слушатель, который обрабатывает результат флоу yandex-pay + * + * @param activity контекст для дальнешей навигации платежного флоу из Activity + * @param fragment экземляр фрагмента - обертки над яндексом + * @param yandexPayRequestCode код для получения результата, по завершению работы экрана Acquiring SDK + * @param onYandexErrorCallback дополнительный метод для возможности обработки ошибки от яндекса на + * стороне клиентского приложения + * @param onYandexCancelCallback дополнительный метод для возможности обработки отмены + */ +fun TinkoffAcquiring.addYandexResultListener( + fragment: YandexButtonFragment, + activity: FragmentActivity, + yandexPayRequestCode: Int, + onYandexErrorCallback: AcqYandexPayErrorCallback? = null, + onYandexCancelCallback: AcqYandexPayCancelCallback? = null +) { + fragment.listener = { + when (it) { + is AcqYandexPayResult.Success -> openYandexPaymentScreen( + activity, + it.paymentOptions, + yandexPayRequestCode, + it.token + ) + is AcqYandexPayResult.Error -> onYandexErrorCallback?.invoke(it.throwable) + else -> onYandexCancelCallback?.invoke() + } + } +} \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexButtonFragment.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexButtonFragment.kt new file mode 100644 index 00000000..0ef82215 --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexButtonFragment.kt @@ -0,0 +1,141 @@ +package ru.tinkoff.acquiring.yandexpay + +import android.os.Bundle +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StyleRes +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.yandex.pay.core.* +import com.yandex.pay.core.data.* +import com.yandex.pay.core.ui.YandexPayButton +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.yandexpay.models.YandexPayData +import ru.tinkoff.acquiring.yandexpay.models.mapYandexOrder + +/** + * Created by i.golovachev + */ +class YandexButtonFragment : Fragment() { + + companion object { + + fun newInstance( + data: YandexPayData, + options: PaymentOptions? = null, + isProd: Boolean = false, + enableLogging: Boolean = false, + @StyleRes themeForYandexButton: Int? = null + ): YandexButtonFragment { + return YandexButtonFragment().apply { + arguments = bundleOf( + YandexButtonFragment::data.name to data, + YandexButtonFragment::options.name to options, + YandexButtonFragment::isProd.name to isProd, + YandexButtonFragment::enableLogging.name to enableLogging, + YandexButtonFragment::theme.name to themeForYandexButton, + ) + } + } + } + + internal var listener: AcqYandexPayCallback? = null + + private val data: YandexPayData by lazy { + arguments?.getSerializable(YandexButtonFragment::data.name) as YandexPayData + } + + lateinit var options: PaymentOptions + + private val theme: Int? by lazy { + arguments?.getInt(YandexButtonFragment::theme.name) + } + + private val enableLogging: Boolean by lazy { + arguments?.getBoolean(YandexButtonFragment::enableLogging.name) ?: false + } + + private val isProd: Boolean by lazy { + arguments?.getBoolean(YandexButtonFragment::isProd.name) ?: false + } + + private val yandexPayLauncher = registerForActivityResult(OpenYandexPayContract()) { result -> + val acqResult = when (result) { + is YandexPayResult.Success -> + AcqYandexPayResult.Success(result.paymentToken.value, options) + is YandexPayResult.Failure -> when (result) { + is YandexPayResult.Failure.Validation -> AcqYandexPayResult.Error(result.details.name) + is YandexPayResult.Failure.Internal -> AcqYandexPayResult.Error(result.message) + } + is YandexPayResult.Cancelled -> AcqYandexPayResult.Cancelled + } + listener?.invoke(acqResult) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val options = arguments?.getParcelable(YandexButtonFragment::options.name) + if (options != null) { + this.options = options + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + initYandexPay( + yadata = data, + yandexPayEnvironment = if (isProd) YandexPayEnvironment.PROD else YandexPayEnvironment.SANDBOX, + logging = enableLogging + ) + + val inf = theme?.let { + inflater.cloneInContext(ContextThemeWrapper(requireContext(), it)) + } ?: inflater + + val button = inf.inflate( + R.layout.acq_view_yandex_pay_button, container, false + ) as YandexPayButton + + button.setOnClickListener { -> + val orderDetails = OrderDetails( + paymentMethods = data.toYandexPayMethods, + order = options.mapYandexOrder() + ) + // запустите сервис с помощью лаунчера, передав сформированные orderDetails + yandexPayLauncher.launch(orderDetails) + } + + return button + } + + private fun initYandexPay( + yadata: YandexPayData, + yandexPayEnvironment: YandexPayEnvironment = YandexPayEnvironment.SANDBOX, + logging: Boolean = true + ) { + val merch = Merchant( + id = MerchantId.from(yadata.merchantId), + name = yadata.merchantName, + url = yadata.merchantUrl + ) + if (YandexPayLib.isSupported) { + YandexPayLib.initialize( + context = requireContext(), + config = YandexPayLibConfig( + merchantDetails = merch, + environment = yandexPayEnvironment, + logging = logging, + locale = YandexPayLocale.SYSTEM, + ) + ) + } else { + throw IllegalAccessException() + } + } +} \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayError.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayError.kt new file mode 100644 index 00000000..aa103f24 --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayError.kt @@ -0,0 +1,6 @@ +package ru.tinkoff.acquiring.yandexpay + +/** + * Created by i.golovachev + */ +class YandexPayError(message: String) : Throwable(message), java.io.Serializable \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayResult.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayResult.kt new file mode 100644 index 00000000..7f824398 --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/YandexPayResult.kt @@ -0,0 +1,16 @@ +package ru.tinkoff.acquiring.yandexpay + +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions + +/** + * Created by i.golovachev + */ +sealed class AcqYandexPayResult { + class Success(val token: String, val paymentOptions: PaymentOptions) : AcqYandexPayResult() + class Error(val throwable: Throwable) : AcqYandexPayResult() { + + constructor(message: String) : this(YandexPayError(message)) + } + + object Cancelled : AcqYandexPayResult() +} \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayCoreExt.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayCoreExt.kt new file mode 100644 index 00000000..8fb4dc70 --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayCoreExt.kt @@ -0,0 +1,52 @@ +package ru.tinkoff.acquiring.yandexpay.models + +import com.yandex.pay.core.data.Amount +import com.yandex.pay.core.data.Order +import com.yandex.pay.core.data.OrderID +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.responses.Paymethod +import ru.tinkoff.acquiring.sdk.responses.TerminalInfo +import ru.tinkoff.acquiring.sdk.utils.Money +import ru.tinkoff.acquiring.sdk.utils.Money.Companion.COINS_IN_RUBLE + +/** + * Created by i.golovachev + */ +fun TerminalInfo.mapYandexPayData(): YandexPayData? { + return paymethods.firstOrNull { it.paymethod == Paymethod.YandexPay } + ?.params + ?.run { + YandexPayData( + merchantId = getValue("ShowcaseId"), + merchantName = getValue("MerchantName"), + merchantUrl = getValue("MerchantOrigin"), + gatewayMerchantId = getValue("MerchantId") + ) + } + ?: return null +} + +fun TerminalInfo.enableYandexPay() = paymethods.any { it.paymethod == Paymethod.YandexPay } + +fun PaymentOptions.mapYandexOrder(): Order { + return Order( + + OrderID.from(this.order.orderId), + + Amount.from(this.order.amount.toYandexString()), + + this.order.description + ) +} + +fun Money.toYandexString(): String { + val fractional = coins.rem(COINS_IN_RUBLE) + val rub = coins.div(COINS_IN_RUBLE) + return String.format("%s%s%02d", + rub, + YANDEX_INT_FRACT_DIVIDER, + fractional + ) +} + +private const val YANDEX_INT_FRACT_DIVIDER = "." \ No newline at end of file diff --git a/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayData.kt b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayData.kt new file mode 100644 index 00000000..2d85fdc2 --- /dev/null +++ b/yandexpay/src/main/java/ru/tinkoff/acquiring/yandexpay/models/YandexPayData.kt @@ -0,0 +1,47 @@ +package ru.tinkoff.acquiring.yandexpay.models + +import com.yandex.pay.core.data.* +import java.io.Serializable + +/** + * Created by i.golovachev + */ +data class YandexPayData internal constructor( + val merchantId: String, + val merchantName: String, + val merchantUrl: String, + val gatewayMerchantId: String, + val gatewayAcqId: GatewayAcqId = GatewayAcqId.tinkoff +) : Serializable { + + internal val allowedAuthMethods = listOf(AuthMethod.PanOnly) + internal val type = PaymentMethodType.Card + internal val gateway = Gateway.from(gatewayAcqId.name) + internal val allowedCardNetworks = listOf( + CardNetwork.Visa, + CardNetwork.MasterCard, + CardNetwork.MIR + ) + internal val gatewayMerchantIdYandex = GatewayMerchantID.from(gatewayMerchantId) + + internal val toYandexPayMethods + get() = listOf( + PaymentMethod( + // Что будет содержаться в платежном токене: зашифрованные данные банковской карты + // или токенизированная карта + allowedAuthMethods, + // Метод оплаты + type, + // ID поставщика платежных услуг + gateway, + // Список поддерживаемых платежных систем + allowedCardNetworks, + // ID продавца в системе поставщика платежных услуг + gatewayMerchantIdYandex, + ), + ) +} + +enum class GatewayAcqId { + tinkoff +} \ No newline at end of file diff --git a/yandexpay/src/main/res/layout/acq_view_yandex_pay_button.xml b/yandexpay/src/main/res/layout/acq_view_yandex_pay_button.xml new file mode 100644 index 00000000..52275ca7 --- /dev/null +++ b/yandexpay/src/main/res/layout/acq_view_yandex_pay_button.xml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/yandexpay/src/main/res/values/styles.xml b/yandexpay/src/main/res/values/styles.xml new file mode 100644 index 00000000..abf0b17e --- /dev/null +++ b/yandexpay/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/GetTerminalPayMethodsTest.kt b/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/GetTerminalPayMethodsTest.kt new file mode 100644 index 00000000..ab7884d2 --- /dev/null +++ b/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/GetTerminalPayMethodsTest.kt @@ -0,0 +1,173 @@ +package ru.tinkoff.acquiring.yandexpay + +import com.yandex.pay.core.data.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.requests.GetTerminalPayMethodsRequest +import ru.tinkoff.acquiring.sdk.responses.GetTerminalPayMethodsResponse +import ru.tinkoff.acquiring.sdk.responses.Paymethod +import ru.tinkoff.acquiring.sdk.responses.PaymethodData +import ru.tinkoff.acquiring.sdk.responses.TerminalInfo +import ru.tinkoff.acquiring.sdk.utils.Money +import ru.tinkoff.acquiring.yandexpay.models.GatewayAcqId +import ru.tinkoff.acquiring.yandexpay.models.mapYandexPayData + +/** + * Created by i.golovachev + */ +class GetTerminalPayMethodsTest { + + @Test + // #2347844 + fun `get terminal without yandex pay`() = runBlocking { + val response = GetTerminalPayMethodsResponse(TerminalInfo()) + val requests = mock { + on { performRequestAsync(any()) } doReturn CompletableDeferred( + Result.success(response) + ) + } + val getTerminalPayMethodsResponse = + requests.performRequestAsync(GetTerminalPayMethodsResponse::class.java) + + Assert.assertNull( + getTerminalPayMethodsResponse.await().getOrThrow().terminalInfo?.mapYandexPayData() + ) + } + + @Test + // 2347847 + fun `get terminal pay with yandex pay`() = runBlocking { + val response = GetTerminalPayMethodsResponse( + TerminalInfo( + paymethods = listOf( + PaymethodData( + paymethod = Paymethod.YandexPay, + params = mapOf( + "ShowcaseId" to "ShowcaseId", + "MerchantName" to "MerchantName", + "MerchantOrigin" to "MerchantOrigin", + "MerchantId" to "MerchantId", + ) + ) + ) + ) + ) + val requests = mock { + on { performRequestAsync(any()) } doReturn CompletableDeferred( + Result.success(response) + ) + } + val getTerminalPayMethodsResponse = + requests.performRequestAsync(GetTerminalPayMethodsResponse::class.java) + + Assert.assertNotNull( + getTerminalPayMethodsResponse.await().getOrNull()?.terminalInfo?.mapYandexPayData() + ) + } + + @Test + //#2355347 + fun `get terminal pay with error`() = runBlocking { + val requests = mock { + on { performRequestAsync(any()) } doReturn CompletableDeferred( + Result.failure(InternalError()) + ) + } + val getTerminalPayMethodsResponse = + requests.performRequestAsync(GetTerminalPayMethodsResponse::class.java) + + Assert.assertNotNull(getTerminalPayMethodsResponse.await().exceptionOrNull()) + } + + @Test + //#2355347 + fun `get terminal pay return data with yandex and more others`() = runBlocking { + val response = GetTerminalPayMethodsResponse( + TerminalInfo( + paymethods = listOf( + PaymethodData( + paymethod = Paymethod.YandexPay, + params = mapOf( + "ShowcaseId" to "ShowcaseId", + "MerchantName" to "MerchantName", + "MerchantOrigin" to "MerchantOrigin", + "MerchantId" to "MerchantId", + ) + ), + PaymethodData( + paymethod = Paymethod.TinkoffPay, + params = mapOf( + "version" to "2.0", + ) + ), + PaymethodData( + paymethod = Paymethod.SBP, + ) + ) + ) + ) + val requests = mock { + on { performRequestAsync(any()) } doReturn CompletableDeferred(Result.success(response)) + } + val getTerminalPayMethodsResponse = + requests.performRequestAsync(GetTerminalPayMethodsResponse::class.java) + + Assert.assertNotNull( + getTerminalPayMethodsResponse.await().getOrNull()?.terminalInfo?.mapYandexPayData() + ) + } + + @Test + //#2348017 + fun `When get terminal pay Then pass is in yandex data`() = runBlocking { + val response = GetTerminalPayMethodsResponse( + TerminalInfo( + paymethods = listOf( + PaymethodData( + paymethod = Paymethod.YandexPay, + params = mapOf( + "ShowcaseId" to "15a919d7-c990-412c-b5eb-8d1ffe60e68a", + "MerchantName" to "Horns and hooves", + "MerchantOrigin" to "https://horns-and-hooves.ru", + "MerchantId" to "123456", + ) + ) + ) + ) + ) + val requests = mock { + on { performRequestAsync(any()) } doReturn CompletableDeferred(Result.success(response)) + } + val getTerminalPayMethodsResponse = + requests.performRequestAsync(GetTerminalPayMethodsResponse::class.java) + val data = checkNotNull( + getTerminalPayMethodsResponse.await().getOrNull()?.terminalInfo?.mapYandexPayData() + ) + val paymentOptions = PaymentOptions() + + paymentOptions.orderOptions { + orderId = "orderId" + amount = Money.ofCoins(1000) + description = "test" + } + + + Assert.assertEquals(data.allowedAuthMethods, listOf(AuthMethod.PanOnly)) + Assert.assertEquals(data.type, PaymentMethodType.Card) + Assert.assertEquals(data.gateway, Gateway.from(GatewayAcqId.tinkoff.name)) + Assert.assertEquals( + data.allowedCardNetworks, listOf( + CardNetwork.Visa, + CardNetwork.MasterCard, + CardNetwork.MIR + ) + ) + Assert.assertEquals(data.gatewayMerchantIdYandex, GatewayMerchantID.from("123456")) + } +} \ No newline at end of file diff --git a/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/MoneyUtilsTest.kt b/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/MoneyUtilsTest.kt new file mode 100644 index 00000000..d1df06e9 --- /dev/null +++ b/yandexpay/src/test/java/ru/tinkoff/acquiring/yandexpay/MoneyUtilsTest.kt @@ -0,0 +1,44 @@ +package ru.tinkoff.acquiring.yandexpay + +import org.junit.Assert +import org.junit.Test +import ru.tinkoff.acquiring.sdk.utils.Money +import ru.tinkoff.acquiring.yandexpay.models.toYandexString + +/** + * Created by i.golovachev + */ +class MoneyUtilsTest { + + @Test + fun `when amount with coins`() { + Assert.assertEquals( + Money.ofCoins(1950).toYandexString(), + "19.50" + ) + } + + @Test + fun `when amount without coins`() { + Assert.assertEquals( + Money.ofCoins(1900).toYandexString(), + "19.00" + ) + } + + @Test + fun `when amount without one coin`() { + Assert.assertEquals( + Money.ofCoins(100901).toYandexString(), + "1009.01" + ) + } + + @Test + fun `when amount its only coin`() { + Assert.assertEquals( + Money.ofCoins(25).toYandexString(), + "0.25" + ) + } +} \ No newline at end of file