From e56c7cce9e6764051efc3731c3f9aee9688c0bdb Mon Sep 17 00:00:00 2001 From: jqwout Date: Mon, 6 Mar 2023 13:46:21 +0300 Subject: [PATCH 1/9] env --- .../ru/tinkoff/acquiring/sdk/AcquiringSdk.kt | 5 ++ .../ui/environment/EnvidonmentDialog.kt | 74 +++++++++++++++++++ .../res/layout/asdk_environment_dialog.xml | 69 +++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt create mode 100644 sample/src/main/res/layout/asdk_environment_dialog.xml 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 ab9bfb6b..732b7e00 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt @@ -284,6 +284,11 @@ class AcquiringSdk( */ var isDeveloperMode = false + /** + * Позволяет переключать SDK на иной апи-контур, работает только в дебаг режиме + */ + var customUrl : String? = null + /** * Логирует сообщение diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt new file mode 100644 index 00000000..f881281d --- /dev/null +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt @@ -0,0 +1,74 @@ +package ru.tinkoff.acquiring.sample.ui.environment + + +import android.os.Bundle +import android.view.* +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioGroup +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import ru.tinkoff.acquiring.sample.R +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.network.AcquiringApi + + + +/** + * Created by i.golovachev + */ +class AcqEnvironmentDialog : DialogFragment() { + + private val description: TextView by lazy { + requireView().findViewById(R.id.acq_env_description) + } + private val ok: TextView by lazy { + requireView().findViewById(R.id.acq_env_ok) + } + private val editUrlText: EditText by lazy { + requireView().findViewById(R.id.acq_env_url) + } + private val evnGroup: RadioGroup by lazy { + requireView().findViewById(R.id.acq_env_urls) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_asdk_env, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + evnGroup.setOnCheckedChangeListener { _, checkedId -> + when(checkedId) { + R.id.acq_env_is_pre_prod_btn -> { + editUrlText.setText(PRE_PROD_URL) + } + R.id.acq_env_is_debug_btn -> { + + } + R.id.acq_env_is_custom_btn -> { + + } + } + } + + ok.setOnClickListener { dismiss() } + } + + override fun onResume() { + dialog?.window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + super.onResume() + } + + + companion object { + private val PRE_PROD_URL = "https://qa-mapi.tcsbank.ru/" + + const val TAG = "AcqEnvironmentDialog" + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/asdk_environment_dialog.xml b/sample/src/main/res/layout/asdk_environment_dialog.xml new file mode 100644 index 00000000..a63fd4b6 --- /dev/null +++ b/sample/src/main/res/layout/asdk_environment_dialog.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + From 0124b43c707f3f8620a520394dc9addf5f758ea0 Mon Sep 17 00:00:00 2001 From: jqwout Date: Mon, 6 Mar 2023 18:16:27 +0300 Subject: [PATCH 2/9] MC-8322 new certs and endpoints --- .../acquiring/sdk/network/AcquiringApi.kt | 50 +++++++- sample/build.gradle | 3 + .../acquiring/sample/SampleApplication.kt | 2 + .../acquiring/sample/ui/MainActivity.kt | 8 +- .../ui/environment/AcqEnvironmentDialog.kt | 112 ++++++++++++++++++ .../ui/environment/EnvidonmentDialog.kt | 74 ------------ .../sample/utils/SettingsSdkManager.kt | 9 ++ .../res/layout/asdk_environment_dialog.xml | 4 +- sample/src/main/res/menu/main_menu.xml | 5 + sample/src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values/preferences_keys.xml | 1 + sample/src/main/res/values/strings.xml | 1 + .../main/res/xml/network_security_config.xml | 8 +- 13 files changed, 192 insertions(+), 86 deletions(-) create mode 100644 sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/AcqEnvironmentDialog.kt delete mode 100644 sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt 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 d3cb081c..f885c28f 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 @@ -57,9 +57,40 @@ object AcquiringApi { /** * Коды ошибок, сообщение которых можно показать конечным пользователям */ - val errorCodesForUserShowing = listOf("53", "206", "224", "225", "252", "99", "101", - "1006", "1012", "1013", "1014", "1015", "1030", "1033", "1034", "1035", "1036", "1037", "1038", - "1039", "1040", "1041", "1042", "1043", "1051", "1054", "1057", "1065", "1082", "1089", "1091", "1096") + val errorCodesForUserShowing = listOf( + "53", + "206", + "224", + "225", + "252", + "99", + "101", + "1006", + "1012", + "1013", + "1014", + "1015", + "1030", + "1033", + "1034", + "1035", + "1036", + "1037", + "1038", + "1039", + "1040", + "1041", + "1042", + "1043", + "1051", + "1054", + "1057", + "1065", + "1082", + "1089", + "1091", + "1096" + ) /** * Коды ошибок, вызванные временными неполадками системы @@ -94,13 +125,22 @@ object AcquiringApi { */ fun getUrl(apiMethod: String): String { return if (useV1Api(apiMethod)) { - if (AcquiringSdk.isDeveloperMode) API_URL_DEBUG_OLD else API_URL_RELEASE_OLD + if (AcquiringSdk.isDeveloperMode) + useCustomOrDefault(API_URL_DEBUG_OLD, AcquiringSdk.customUrl, "rest") + else + API_URL_RELEASE_OLD } else { - if (AcquiringSdk.isDeveloperMode) API_URL_DEBUG else API_URL_RELEASE + if (AcquiringSdk.isDeveloperMode) + useCustomOrDefault(API_URL_DEBUG, AcquiringSdk.customUrl) + else + API_URL_RELEASE } } internal fun useV1Api(apiMethod: String): Boolean { return oldMethodsList.contains(apiMethod) } + + private fun useCustomOrDefault(default: String, custom: String?, oldOrV2: String = "v2") = + custom?.let { "$it/$oldOrV2" } ?: default } \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 04ab21e2..0e5da7df 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -42,6 +42,9 @@ android { useJUnitPlatform() } } + lintOptions { + disable 'NetworkSecurityConfig' + } } dependencies { diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt index 12f2b3dd..60a2acb1 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt @@ -19,6 +19,7 @@ package ru.tinkoff.acquiring.sample import android.app.Application import android.content.Context import ru.tinkoff.acquiring.sample.utils.SessionParams +import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager import ru.tinkoff.acquiring.sdk.AcquiringSdk import ru.tinkoff.acquiring.sdk.TinkoffAcquiring @@ -36,6 +37,7 @@ class SampleApplication : Application() { initSdk(this, TerminalsManager.init(this).selectedTerminal) AcquiringSdk.isDeveloperMode = true AcquiringSdk.isDebug = true + AcquiringSdk.customUrl = SettingsSdkManager(this).customUrl } override fun onTerminate() { diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/MainActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/MainActivity.kt index b817dbe4..af4d57ac 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/MainActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/MainActivity.kt @@ -26,8 +26,6 @@ import android.view.MenuItem import android.widget.ListView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.SampleApplication import ru.tinkoff.acquiring.sample.adapters.BooksListAdapter @@ -35,8 +33,8 @@ import ru.tinkoff.acquiring.sample.models.Book import ru.tinkoff.acquiring.sample.models.BooksRegistry import ru.tinkoff.acquiring.sample.service.PaymentNotificationIntentService import ru.tinkoff.acquiring.sample.service.PriceNotificationReceiver +import ru.tinkoff.acquiring.sample.ui.environment.AcqEnvironmentDialog import ru.tinkoff.acquiring.sample.utils.PaymentNotificationManager -import ru.tinkoff.acquiring.sample.utils.SessionParams import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.EXTRA_CARD_ID @@ -126,6 +124,10 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe AboutActivity.start(this) true } + R.id.menu_action_environment -> { + AcqEnvironmentDialog().show(supportFragmentManager, AttachCardManuallyDialogFragment.TAG) + true + } R.id.menu_action_static_qr -> { openStaticQrScreen() true diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/AcqEnvironmentDialog.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/AcqEnvironmentDialog.kt new file mode 100644 index 00000000..ceb36a13 --- /dev/null +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/AcqEnvironmentDialog.kt @@ -0,0 +1,112 @@ +package ru.tinkoff.acquiring.sample.ui.environment + + +import android.os.Bundle +import android.view.* +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioGroup +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import kotlinx.android.synthetic.main.payment_notification.view.* +import ru.tinkoff.acquiring.sample.R +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.network.AcquiringApi + + +/** + * Created by i.golovachev + */ +class AcqEnvironmentDialog : DialogFragment() { + + private val ok: TextView by lazy { + requireView().findViewById(R.id.acq_env_ok) + } + private val editUrlText: EditText by lazy { + requireView().findViewById(R.id.acq_env_url) + } + private val evnGroup: RadioGroup by lazy { + requireView().findViewById(R.id.acq_env_urls) + } + private var customUrl: String? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.asdk_environment_dialog, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + evnGroup.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.acq_env_is_pre_prod_btn -> { + editUrlText.setText(PRE_PROD_URL) + editUrlText.isEnabled = false + customUrl = PRE_PROD_URL + } + R.id.acq_env_is_debug_btn -> { + customUrl = null + editUrlText.isEnabled = false + editUrlText.setText(AcquiringApi.getUrl("/")) + } + R.id.acq_env_is_custom_btn -> { + customUrl = null + editUrlText.setText("https://") + editUrlText.isEnabled = true + editUrlText.requestFocus() + } + } + } + + setupEnv() + + ok.setOnClickListener { + customUrl = editUrlText.text.toString() + AcquiringSdk.customUrl = customUrl + dismiss() + } + } + + override fun onResume() { + dialog?.window?.setLayout( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + super.onResume() + } + + + private fun setupEnv() { + val isDebug = AcquiringSdk.isDeveloperMode + val customUrl = AcquiringSdk.customUrl + + when { + customUrl != null && isDebug -> { + if (customUrl.contains(PRE_PROD_URL)) { + evnGroup.check(R.id.acq_env_is_pre_prod_btn) + } else { + evnGroup.check(R.id.acq_env_is_custom_btn) + } + editUrlText.setText(customUrl) + } + isDebug -> { + evnGroup.check(R.id.acq_env_is_debug_btn) + editUrlText.setText(AcquiringApi.getUrl("/")) + } + else -> { + evnGroup.check(-1) + } + } + + } + + companion object { + private val PRE_PROD_URL = "https://qa-mapi.tcsbank.ru" + + const val TAG = "AcqEnvironmentDialog" + } +} \ No newline at end of file diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt deleted file mode 100644 index f881281d..00000000 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/environment/EnvidonmentDialog.kt +++ /dev/null @@ -1,74 +0,0 @@ -package ru.tinkoff.acquiring.sample.ui.environment - - -import android.os.Bundle -import android.view.* -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.RadioGroup -import android.widget.TextView -import androidx.fragment.app.DialogFragment -import ru.tinkoff.acquiring.sample.R -import ru.tinkoff.acquiring.sdk.AcquiringSdk -import ru.tinkoff.acquiring.sdk.network.AcquiringApi - - - -/** - * Created by i.golovachev - */ -class AcqEnvironmentDialog : DialogFragment() { - - private val description: TextView by lazy { - requireView().findViewById(R.id.acq_env_description) - } - private val ok: TextView by lazy { - requireView().findViewById(R.id.acq_env_ok) - } - private val editUrlText: EditText by lazy { - requireView().findViewById(R.id.acq_env_url) - } - private val evnGroup: RadioGroup by lazy { - requireView().findViewById(R.id.acq_env_urls) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.dialog_asdk_env, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - evnGroup.setOnCheckedChangeListener { _, checkedId -> - when(checkedId) { - R.id.acq_env_is_pre_prod_btn -> { - editUrlText.setText(PRE_PROD_URL) - } - R.id.acq_env_is_debug_btn -> { - - } - R.id.acq_env_is_custom_btn -> { - - } - } - } - - ok.setOnClickListener { dismiss() } - } - - override fun onResume() { - dialog?.window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) - super.onResume() - } - - - companion object { - private val PRE_PROD_URL = "https://qa-mapi.tcsbank.ru/" - - const val TAG = "AcqEnvironmentDialog" - } -} \ No newline at end of file diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SettingsSdkManager.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SettingsSdkManager.kt index d737ca4b..9d1c7fc2 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SettingsSdkManager.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SettingsSdkManager.kt @@ -72,6 +72,15 @@ class SettingsSdkManager(private val context: Context) { val handleCardListErrorInSdk: Boolean get() = preferences.getBoolean(context.getString(R.string.acq_sp_handle_cards_error), true) + var customUrl: String? + set(value) { + preferences.edit().apply { + putString(context.getString(R.string.acq_sp_custom_url), value) + } + .commit() + } + get() = preferences.getString(context.getString(R.string.acq_sp_custom_url), null) + private val styleName: String? get() { val defaultStyleName = context.getString(R.string.acq_sp_default_style_id) diff --git a/sample/src/main/res/layout/asdk_environment_dialog.xml b/sample/src/main/res/layout/asdk_environment_dialog.xml index a63fd4b6..ea61f4b8 100644 --- a/sample/src/main/res/layout/asdk_environment_dialog.xml +++ b/sample/src/main/res/layout/asdk_environment_dialog.xml @@ -24,6 +24,7 @@ @@ -53,11 +54,12 @@ + + Привязать карту вручную Терминалы О программе + Окружение Открыть статический QR код Принять оплату QR Настройки diff --git a/sample/src/main/res/values/preferences_keys.xml b/sample/src/main/res/values/preferences_keys.xml index 7e2ec691..26de40a7 100644 --- a/sample/src/main/res/values/preferences_keys.xml +++ b/sample/src/main/res/values/preferences_keys.xml @@ -60,4 +60,5 @@ TEST-DEMO + acq_sp_custom_url \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 8478bb99..26e3b5b2 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Attach card manually Terminals About + Environment Open static QR code Get QR payment Settings diff --git a/sample/src/main/res/xml/network_security_config.xml b/sample/src/main/res/xml/network_security_config.xml index e8ab97bf..ea785aeb 100644 --- a/sample/src/main/res/xml/network_security_config.xml +++ b/sample/src/main/res/xml/network_security_config.xml @@ -1,9 +1,11 @@ - - - + + + + + From 24a469d00f6f6a5454b3457ec7ddefd64d88b29d Mon Sep 17 00:00:00 2001 From: jqwout Date: Tue, 21 Mar 2023 16:54:30 +0300 Subject: [PATCH 3/9] =?UTF-8?q?EACQAPW-4082=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=B0=D0=BB=D0=B3?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=B0=20=D0=BE=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B0=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sdk/exceptions/AcquiringSdkException.kt | 2 +- .../AcquiringSdkTimeoutException.kt | 30 ++++++++++ .../sdk/models/enums/ResponseStatus.kt | 3 + .../acquiring/sdk/network/NetworkClient.kt | 6 +- .../acquiring/sample/ui/PayableActivity.kt | 30 ++++++++-- .../sdk/payment/pooling/GetStatusMethod.kt | 20 +++++++ .../sdk/payment/pooling/GetStatusPooling.kt | 47 ++++++++++++++++ .../sdk/ui/activities/AttachCardActivity.kt | 1 + .../acquiring/sdk/utils/CoroutineManager.kt | 17 ++---- .../ru/tinkoff/acquiring/sdk/utils/FlowExt.kt | 11 ++++ .../sdk/viewmodel/PaymentViewModel.kt | 56 +++++++------------ .../sdk/viewmodel/ThreeDsViewModel.kt | 48 +++++++--------- .../payment/pooling/GetStatusPolingTest.kt | 38 +++++++++++++ 13 files changed, 225 insertions(+), 84 deletions(-) create mode 100644 core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt create mode 100644 ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt index 337637b9..2e74d6ab 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt @@ -21,4 +21,4 @@ package ru.tinkoff.acquiring.sdk.exceptions * * @author Mariya Chernyadieva */ -class AcquiringSdkException(throwable: Throwable) : RuntimeException(throwable.message, throwable) \ No newline at end of file +class AcquiringSdkException(throwable: Throwable, paymentId: Long? = null) : RuntimeException(throwable.message, throwable) \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt new file mode 100644 index 00000000..c07ea939 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt @@ -0,0 +1,30 @@ +/* + * 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.exceptions + +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus + +/** + * Исключение, выбрасываемое в случае, когда ожидание статуса платежа истекло + * + * @author i.golovachev + */ +class AcquiringSdkTimeoutException( + val throwable: Throwable, + val paymentId: Long?, + val status: ResponseStatus?, +) : RuntimeException(throwable.message, throwable) \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt index d3e4bdc2..eee8727e 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt @@ -39,6 +39,7 @@ enum class ResponseStatus { REFUNDED, PARTIAL_REFUNDED, REJECTED, + DEADLINE_EXPIRED, UNKNOWN, LOOP_CHECKING, COMPLETED, @@ -57,6 +58,8 @@ enum class ResponseStatus { private const val TDS_CHECKING_STRING = "3DS_CHECKING" private const val TDS_CHECKED_STRING = "3DS_CHECKED" + val successStatuses = setOf(CONFIRMED,AUTHORIZED) + fun checkSuccessStatuses(status: ResponseStatus) : Boolean = status in successStatuses @JvmStatic fun fromString(stringValue: String): ResponseStatus { 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 efc3d00c..adc9f1a4 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 @@ -89,7 +89,7 @@ internal class NetworkClient { onFailure( AcquiringApiException( result, - "${result.message ?: ""} ${result.details ?: ""}" + makeNetworkErrorMessage(result.message, result.details) ) ) } @@ -185,6 +185,10 @@ internal class NetworkClient { return URL(builder.toString()) } + private fun makeNetworkErrorMessage(message : String?, details: String?): String { + return setOf(message.orEmpty(), details.orEmpty()).joinToString() + } + companion object { fun createGson(): Gson { diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index 92aeae45..1efa9b7f 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -28,20 +28,19 @@ import androidx.core.view.isVisible import androidx.fragment.app.commit import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.SampleApplication -import ru.tinkoff.acquiring.sample.utils.SessionParams import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager +import ru.tinkoff.acquiring.sdk.AcquiringSdk.Companion.log import ru.tinkoff.acquiring.sdk.TinkoffAcquiring import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.RESULT_ERROR +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkTimeoutException import ru.tinkoff.acquiring.sdk.localization.AsdkSource import ru.tinkoff.acquiring.sdk.localization.Language import ru.tinkoff.acquiring.sdk.models.AsdkState -import ru.tinkoff.acquiring.sdk.models.GooglePayParams import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions import ru.tinkoff.acquiring.sdk.payment.PaymentListener import ru.tinkoff.acquiring.sdk.payment.PaymentListenerAdapter import ru.tinkoff.acquiring.sdk.payment.PaymentState -import ru.tinkoff.acquiring.sdk.utils.GooglePayHelper import ru.tinkoff.acquiring.sdk.utils.Money import ru.tinkoff.acquiring.yandexpay.YandexButtonFragment import ru.tinkoff.acquiring.yandexpay.addYandexResultListener @@ -270,7 +269,10 @@ open class PayableActivity : AppCompatActivity() { RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable)?.printStackTrace() + getErrorFromIntent(data)?.run { + printStackTrace() + logIfTimeout() + } } } } @@ -283,7 +285,10 @@ open class PayableActivity : AppCompatActivity() { RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable)?.printStackTrace() + getErrorFromIntent(data)?.run { + printStackTrace() + logIfTimeout() + } } } } @@ -357,6 +362,21 @@ open class PayableActivity : AppCompatActivity() { isProgressShowing = false } + private fun getErrorFromIntent(data: Intent?): Throwable? { + return (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable) + } + + private fun Throwable.logIfTimeout() { + (this as? AcquiringSdkTimeoutException)?.run { + if (paymentId != null) { + log("paymentId : $paymentId") + } + if (status != null) { + log("status : $status") + } + } + } + companion object { const val PAYMENT_REQUEST_CODE = 1 diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt new file mode 100644 index 00000000..0586a949 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt @@ -0,0 +1,20 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest + +/** + * Created by i.golovachev + */ +fun interface GetStatusMethod { + suspend operator fun invoke(paymentId: Long): ResponseStatus? + + class Impl(private val acquiringSdk: AcquiringSdk) : GetStatusMethod { + + override suspend fun invoke(paymentId: Long): ResponseStatus? = + // ignore errors + acquiringSdk.getState { this.paymentId = paymentId }.performSuspendRequest() + .getOrNull()?.status + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt new file mode 100644 index 00000000..e86ac9be --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt @@ -0,0 +1,47 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkTimeoutException +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus +import ru.tinkoff.acquiring.sdk.utils.emitNotNull + +class GetStatusPoling(private val getStatusMethod: GetStatusMethod) { + + constructor(asdk: AcquiringSdk) : this(GetStatusMethod.Impl(asdk)) + + fun start(retriesCount: Int = POLLING_RETRIES_COUNT, paymentId: Long): Flow { + return flow { + var tries = 0 + while (retriesCount > tries) { + val status = getStatusMethod(paymentId) + when (status) { + in ResponseStatus.successStatuses -> { + return@flow + } + ResponseStatus.REJECTED -> { + throw AcquiringSdkException(IllegalStateException("PaymentState = $status"), paymentId) + } + ResponseStatus.DEADLINE_EXPIRED -> { + throw AcquiringSdkTimeoutException(IllegalStateException("PaymentState = $status"), paymentId, status) + } + else -> { + tries += 1 + } + } + emitNotNull(status) + delay(POLLING_DELAY_MS) + } + + throw AcquiringSdkTimeoutException(IllegalStateException("timeout, retries count is over"), paymentId, null) + } + } + + companion object { + private const val POLLING_DELAY_MS = 3000L + private const val POLLING_RETRIES_COUNT = 10 + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt index 0aefef59..42bab542 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt @@ -90,6 +90,7 @@ internal class AttachCardActivity : TransparentActivity() { } } is LoopConfirmationScreenState -> showFragment(LoopConfirmationFragment.newInstance(screen.requestKey)) + else -> Unit } } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt index eb85a495..97e114cb 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt @@ -16,15 +16,8 @@ package ru.tinkoff.acquiring.sdk.utils -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -89,14 +82,14 @@ internal class CoroutineManager(private val exceptionHandler: (Throwable) -> Uni } } - fun launchOnMain(block: suspend CoroutineScope.() -> Unit) { - coroutineScope.launch(Dispatchers.Main) { + fun launchOnMain(block: suspend CoroutineScope.() -> Unit): Job { + return coroutineScope.launch(Dispatchers.Main) { block.invoke(this) } } - fun launchOnBackground(block: suspend CoroutineScope.() -> Unit) { - coroutineScope.launch(IO) { + fun launchOnBackground(block: suspend CoroutineScope.() -> Unit): Job { + return coroutineScope.launch(IO) { block.invoke(this) } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt new file mode 100644 index 00000000..579828eb --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt @@ -0,0 +1,11 @@ +package ru.tinkoff.acquiring.sdk.utils + +import kotlinx.coroutines.flow.FlowCollector + +/** + * Created by i.golovachev + */ +suspend fun FlowCollector.emitNotNull(state: T?) { + state ?: return + emit(state) +} \ No newline at end of file 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 f5b16d76..dc9dcda5 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 @@ -19,16 +19,19 @@ package ru.tinkoff.acquiring.sdk.viewmodel import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import ru.tinkoff.acquiring.sdk.AcquiringSdk -import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.AsdkState import ru.tinkoff.acquiring.sdk.models.BrowseFpsBankScreenState import ru.tinkoff.acquiring.sdk.models.BrowseFpsBankState import ru.tinkoff.acquiring.sdk.models.Card import ru.tinkoff.acquiring.sdk.models.DefaultScreenState import ru.tinkoff.acquiring.sdk.models.FinishWithErrorScreenState -import ru.tinkoff.acquiring.sdk.models.FpsBankFormShowedScreenState import ru.tinkoff.acquiring.sdk.models.FpsScreenState import ru.tinkoff.acquiring.sdk.models.FpsState import ru.tinkoff.acquiring.sdk.models.LoadedState @@ -48,6 +51,7 @@ import ru.tinkoff.acquiring.sdk.models.result.PaymentResult import ru.tinkoff.acquiring.sdk.payment.PaymentListener import ru.tinkoff.acquiring.sdk.payment.PaymentListenerAdapter import ru.tinkoff.acquiring.sdk.payment.PaymentProcess +import ru.tinkoff.acquiring.sdk.payment.pooling.GetStatusPoling import ru.tinkoff.acquiring.sdk.responses.TinkoffPayStatusResponse /** @@ -59,6 +63,7 @@ internal class PaymentViewModel( sdk: AcquiringSdk ) : BaseAcquiringViewModel(application, handleErrorsInSdk, sdk) { + private val getStatusPooling = GetStatusPoling(sdk) private val paymentResult: MutableLiveData = MutableLiveData() private var cardsResult: MutableLiveData> = MutableLiveData() private var tinkoffPayStatusResult: MutableLiveData = MutableLiveData() @@ -66,7 +71,7 @@ internal class PaymentViewModel( private val paymentListener: PaymentListener = createPaymentListener() private val paymentProcess: PaymentProcess = PaymentProcess(sdk, context) - private var requestPaymentStateCount = 0 + private var requestStateJob: Job? = null val paymentResultLiveData: LiveData = paymentResult val cardsResultLiveData: LiveData> = cardsResult @@ -178,42 +183,19 @@ internal class PaymentViewModel( } fun requestPaymentState(paymentId: Long) { - val request = sdk.getState { - this.paymentId = paymentId + requestStateJob?.cancel() + requestStateJob = coroutine.launchOnMain { + getStatusPooling.start(paymentId = paymentId) + .flowOn(Dispatchers.IO) + .catch { handleException(it) } + .filter { ResponseStatus.checkSuccessStatuses(it) } + .collect { handleConfirmOnAuthStatus(paymentId)} } + } - coroutine.call(request, - onSuccess = { response -> - requestPaymentStateCount++ - when (response.status) { - ResponseStatus.CONFIRMED, ResponseStatus.AUTHORIZED -> { - paymentResult.value = PaymentResult(response.paymentId) - requestPaymentStateCount = 0 - changeScreenState(LoadedState) - } - ResponseStatus.FORM_SHOWED -> { - requestPaymentStateCount = 0 - changeScreenState(LoadedState) - changeScreenState(FpsBankFormShowedScreenState(paymentId)) - } - else -> { - if (requestPaymentStateCount == 1) { - changeScreenState(LoadingState) - coroutine.runWithDelay(1000) { - requestPaymentState(paymentId) - } - } else { - changeScreenState(LoadedState) - val throwable = AcquiringSdkException(IllegalStateException("PaymentState = ${response.status}")) - handleException(throwable) - } - } - } - }, - onFailure = { - requestPaymentStateCount = 0 - handleException(it) - }) + private fun handleConfirmOnAuthStatus(paymentId: Long) { + paymentResult.postValue(PaymentResult(paymentId)) + changeScreenState(LoadedState) } private fun createPaymentListener(): PaymentListener { diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt index b9987498..3de53449 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt @@ -19,10 +19,9 @@ package ru.tinkoff.acquiring.sdk.viewmodel import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.emvco3ds.sdk.spec.CompletionEvent -import com.emvco3ds.sdk.spec.ProtocolErrorEvent -import com.emvco3ds.sdk.spec.RuntimeErrorEvent -import com.emvco3ds.sdk.spec.Transaction +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* import ru.tinkoff.acquiring.sdk.AcquiringSdk import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.LoadedState @@ -32,15 +31,7 @@ import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus import ru.tinkoff.acquiring.sdk.models.result.AsdkResult import ru.tinkoff.acquiring.sdk.models.result.CardResult import ru.tinkoff.acquiring.sdk.models.result.PaymentResult -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper.cleanupSafe -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusCanceled -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusError -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusSuccess -import ru.tinkoff.acquiring.sdk.ui.activities.BaseAcquiringActivity -import ru.tinkoff.core.components.threedswrapper.ChallengeStatusReceiverAdapter -import ru.tinkoff.core.components.threedswrapper.ThreeDSWrapper -import ru.tinkoff.core.components.threedswrapper.ThreeDSWrapper.Companion.closeSafe +import ru.tinkoff.acquiring.sdk.payment.pooling.GetStatusPoling internal class ThreeDsViewModel( application: Application, @@ -48,7 +39,9 @@ internal class ThreeDsViewModel( sdk: AcquiringSdk ) : BaseAcquiringViewModel(application, handleErrorsInSdk, sdk) { + private val getStatusPooling = GetStatusPoling(sdk) private val asdkResult: MutableLiveData = MutableLiveData() + private var requestPaymentStateJob: Job? = null val resultLiveData: LiveData = asdkResult fun submitAuthorization(threeDsData: ThreeDsData, transStatus: String) { @@ -73,22 +66,15 @@ internal class ThreeDsViewModel( } fun requestPaymentState(paymentId: Long?) { + requestPaymentStateJob?.cancel() changeScreenState(LoadingState) - - val request = sdk.getState { - this.paymentId = paymentId + requestPaymentStateJob = coroutine.launchOnMain { + getStatusPooling.start(paymentId = paymentId!!) + .flowOn(Dispatchers.IO) + .catch { handleException(it) } + .filter { ResponseStatus.checkSuccessStatuses(it) } + .collect { handleConfirmOnAuthStatus(paymentId)} } - - coroutine.call(request, - onSuccess = { response -> - if (response.status == ResponseStatus.CONFIRMED || response.status == ResponseStatus.AUTHORIZED) { - asdkResult.value = PaymentResult(response.paymentId) - } else { - val throwable = AcquiringSdkException(IllegalStateException("PaymentState = ${response.status}")) - handleException(throwable) - } - changeScreenState(LoadedState) - }) } fun requestAddCardState(requestKey: String?) { @@ -103,10 +89,16 @@ internal class ThreeDsViewModel( if (response.status == ResponseStatus.COMPLETED) { asdkResult.value = CardResult(response.cardId) } else { - val throwable = AcquiringSdkException(IllegalStateException("AsdkState = ${response.status}")) + val throwable = + AcquiringSdkException(IllegalStateException("AsdkState = ${response.status}")) handleException(throwable) } changeScreenState(LoadedState) }) } + + private fun handleConfirmOnAuthStatus(paymentId: Long) { + asdkResult.value = PaymentResult(paymentId) + changeScreenState(LoadedState) + } } \ No newline at end of file diff --git a/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt b/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt new file mode 100644 index 00000000..c2078f10 --- /dev/null +++ b/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt @@ -0,0 +1,38 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Test +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus + +class GetStatusPolingTest { + + @Test + fun start() = runBlocking { + GetStatusPoling { ResponseStatus.AUTHORIZED } + .start(paymentId = 1L) + .collect { println(it) } + } + + @Test + fun start2() = runBlocking { + val status = ResponseStatus.REJECTED + GetStatusPoling { status } + .start(paymentId = 1L) + .catch { + Assert.assertEquals(it.message, "PaymentState = $status") + } + .collect {} + } + + @Test + fun star3() = runBlocking { + GetStatusPoling { null } + .start(paymentId = 1L) + .catch { + Assert.assertEquals(it.message, "timeout, retries count is over") + } + .collect { println(it) } + } +} \ No newline at end of file From c71d658a21bf60708acdb09acead75594bfe15d0 Mon Sep 17 00:00:00 2001 From: jqwout Date: Tue, 21 Mar 2023 16:54:30 +0300 Subject: [PATCH 4/9] =?UTF-8?q?EACQAPW-4082=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=B0=D0=BB=D0=B3?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=B0=20=D0=BE=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B0=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sdk/exceptions/AcquiringSdkException.kt | 2 +- .../AcquiringSdkTimeoutException.kt | 30 ++++++++++ .../sdk/models/enums/ResponseStatus.kt | 3 + .../acquiring/sdk/network/NetworkClient.kt | 6 +- gradle.properties | 4 +- .../acquiring/sample/ui/PayableActivity.kt | 30 ++++++++-- .../sdk/payment/pooling/GetStatusMethod.kt | 20 +++++++ .../sdk/payment/pooling/GetStatusPooling.kt | 47 ++++++++++++++++ .../sdk/ui/activities/AttachCardActivity.kt | 1 + .../acquiring/sdk/utils/CoroutineManager.kt | 17 ++---- .../ru/tinkoff/acquiring/sdk/utils/FlowExt.kt | 11 ++++ .../sdk/viewmodel/PaymentViewModel.kt | 56 +++++++------------ .../sdk/viewmodel/ThreeDsViewModel.kt | 48 +++++++--------- .../payment/pooling/GetStatusPolingTest.kt | 38 +++++++++++++ 14 files changed, 227 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt create mode 100644 ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt index 337637b9..2e74d6ab 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkException.kt @@ -21,4 +21,4 @@ package ru.tinkoff.acquiring.sdk.exceptions * * @author Mariya Chernyadieva */ -class AcquiringSdkException(throwable: Throwable) : RuntimeException(throwable.message, throwable) \ No newline at end of file +class AcquiringSdkException(throwable: Throwable, paymentId: Long? = null) : RuntimeException(throwable.message, throwable) \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt new file mode 100644 index 00000000..c07ea939 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt @@ -0,0 +1,30 @@ +/* + * 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.exceptions + +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus + +/** + * Исключение, выбрасываемое в случае, когда ожидание статуса платежа истекло + * + * @author i.golovachev + */ +class AcquiringSdkTimeoutException( + val throwable: Throwable, + val paymentId: Long?, + val status: ResponseStatus?, +) : RuntimeException(throwable.message, throwable) \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt index d3e4bdc2..eee8727e 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/enums/ResponseStatus.kt @@ -39,6 +39,7 @@ enum class ResponseStatus { REFUNDED, PARTIAL_REFUNDED, REJECTED, + DEADLINE_EXPIRED, UNKNOWN, LOOP_CHECKING, COMPLETED, @@ -57,6 +58,8 @@ enum class ResponseStatus { private const val TDS_CHECKING_STRING = "3DS_CHECKING" private const val TDS_CHECKED_STRING = "3DS_CHECKED" + val successStatuses = setOf(CONFIRMED,AUTHORIZED) + fun checkSuccessStatuses(status: ResponseStatus) : Boolean = status in successStatuses @JvmStatic fun fromString(stringValue: String): ResponseStatus { 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 efc3d00c..adc9f1a4 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 @@ -89,7 +89,7 @@ internal class NetworkClient { onFailure( AcquiringApiException( result, - "${result.message ?: ""} ${result.details ?: ""}" + makeNetworkErrorMessage(result.message, result.details) ) ) } @@ -185,6 +185,10 @@ internal class NetworkClient { return URL(builder.toString()) } + private fun makeNetworkErrorMessage(message : String?, details: String?): String { + return setOf(message.orEmpty(), details.orEmpty()).joinToString() + } + companion object { fun createGson(): Gson { diff --git a/gradle.properties b/gradle.properties index d5e181c1..3fde181b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=2.13.0 -VERSION_CODE=20 +VERSION_NAME=2.13.1 +VERSION_CODE=21 GROUP=ru.tinkoff.acquiring POM_DESCRIPTION=Library which allows you to use internet acquiring in your android app diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index 92aeae45..1efa9b7f 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -28,20 +28,19 @@ import androidx.core.view.isVisible import androidx.fragment.app.commit import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.SampleApplication -import ru.tinkoff.acquiring.sample.utils.SessionParams import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager +import ru.tinkoff.acquiring.sdk.AcquiringSdk.Companion.log import ru.tinkoff.acquiring.sdk.TinkoffAcquiring import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.RESULT_ERROR +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkTimeoutException import ru.tinkoff.acquiring.sdk.localization.AsdkSource import ru.tinkoff.acquiring.sdk.localization.Language import ru.tinkoff.acquiring.sdk.models.AsdkState -import ru.tinkoff.acquiring.sdk.models.GooglePayParams import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions import ru.tinkoff.acquiring.sdk.payment.PaymentListener import ru.tinkoff.acquiring.sdk.payment.PaymentListenerAdapter import ru.tinkoff.acquiring.sdk.payment.PaymentState -import ru.tinkoff.acquiring.sdk.utils.GooglePayHelper import ru.tinkoff.acquiring.sdk.utils.Money import ru.tinkoff.acquiring.yandexpay.YandexButtonFragment import ru.tinkoff.acquiring.yandexpay.addYandexResultListener @@ -270,7 +269,10 @@ open class PayableActivity : AppCompatActivity() { RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable)?.printStackTrace() + getErrorFromIntent(data)?.run { + printStackTrace() + logIfTimeout() + } } } } @@ -283,7 +285,10 @@ open class PayableActivity : AppCompatActivity() { RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable)?.printStackTrace() + getErrorFromIntent(data)?.run { + printStackTrace() + logIfTimeout() + } } } } @@ -357,6 +362,21 @@ open class PayableActivity : AppCompatActivity() { isProgressShowing = false } + private fun getErrorFromIntent(data: Intent?): Throwable? { + return (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable) + } + + private fun Throwable.logIfTimeout() { + (this as? AcquiringSdkTimeoutException)?.run { + if (paymentId != null) { + log("paymentId : $paymentId") + } + if (status != null) { + log("status : $status") + } + } + } + companion object { const val PAYMENT_REQUEST_CODE = 1 diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt new file mode 100644 index 00000000..0586a949 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusMethod.kt @@ -0,0 +1,20 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest + +/** + * Created by i.golovachev + */ +fun interface GetStatusMethod { + suspend operator fun invoke(paymentId: Long): ResponseStatus? + + class Impl(private val acquiringSdk: AcquiringSdk) : GetStatusMethod { + + override suspend fun invoke(paymentId: Long): ResponseStatus? = + // ignore errors + acquiringSdk.getState { this.paymentId = paymentId }.performSuspendRequest() + .getOrNull()?.status + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt new file mode 100644 index 00000000..215ad149 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPooling.kt @@ -0,0 +1,47 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException +import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkTimeoutException +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus +import ru.tinkoff.acquiring.sdk.utils.emitNotNull + +class GetStatusPoling(private val getStatusMethod: GetStatusMethod) { + + constructor(asdk: AcquiringSdk) : this(GetStatusMethod.Impl(asdk)) + + fun start(retriesCount: Int = POLLING_RETRIES_COUNT, delayMs: Long = POLLING_DELAY_MS, paymentId: Long): Flow { + return flow { + var tries = 0 + while (retriesCount > tries) { + val status = getStatusMethod(paymentId) + emitNotNull(status) + when (status) { + in ResponseStatus.successStatuses -> { + return@flow + } + ResponseStatus.REJECTED -> { + throw AcquiringSdkException(IllegalStateException("PaymentState = $status"), paymentId) + } + ResponseStatus.DEADLINE_EXPIRED -> { + throw AcquiringSdkTimeoutException(IllegalStateException("PaymentState = $status"), paymentId, status) + } + else -> { + tries += 1 + } + } + delay(delayMs) + } + + throw AcquiringSdkTimeoutException(IllegalStateException("timeout, retries count is over"), paymentId, null) + } + } + + companion object { + private const val POLLING_DELAY_MS = 3000L + private const val POLLING_RETRIES_COUNT = 10 + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt index 0aefef59..42bab542 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/AttachCardActivity.kt @@ -90,6 +90,7 @@ internal class AttachCardActivity : TransparentActivity() { } } is LoopConfirmationScreenState -> showFragment(LoopConfirmationFragment.newInstance(screen.requestKey)) + else -> Unit } } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt index eb85a495..97e114cb 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/CoroutineManager.kt @@ -16,15 +16,8 @@ package ru.tinkoff.acquiring.sdk.utils -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -89,14 +82,14 @@ internal class CoroutineManager(private val exceptionHandler: (Throwable) -> Uni } } - fun launchOnMain(block: suspend CoroutineScope.() -> Unit) { - coroutineScope.launch(Dispatchers.Main) { + fun launchOnMain(block: suspend CoroutineScope.() -> Unit): Job { + return coroutineScope.launch(Dispatchers.Main) { block.invoke(this) } } - fun launchOnBackground(block: suspend CoroutineScope.() -> Unit) { - coroutineScope.launch(IO) { + fun launchOnBackground(block: suspend CoroutineScope.() -> Unit): Job { + return coroutineScope.launch(IO) { block.invoke(this) } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt new file mode 100644 index 00000000..579828eb --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/FlowExt.kt @@ -0,0 +1,11 @@ +package ru.tinkoff.acquiring.sdk.utils + +import kotlinx.coroutines.flow.FlowCollector + +/** + * Created by i.golovachev + */ +suspend fun FlowCollector.emitNotNull(state: T?) { + state ?: return + emit(state) +} \ No newline at end of file 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 f5b16d76..dc9dcda5 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 @@ -19,16 +19,19 @@ package ru.tinkoff.acquiring.sdk.viewmodel import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import ru.tinkoff.acquiring.sdk.AcquiringSdk -import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.AsdkState import ru.tinkoff.acquiring.sdk.models.BrowseFpsBankScreenState import ru.tinkoff.acquiring.sdk.models.BrowseFpsBankState import ru.tinkoff.acquiring.sdk.models.Card import ru.tinkoff.acquiring.sdk.models.DefaultScreenState import ru.tinkoff.acquiring.sdk.models.FinishWithErrorScreenState -import ru.tinkoff.acquiring.sdk.models.FpsBankFormShowedScreenState import ru.tinkoff.acquiring.sdk.models.FpsScreenState import ru.tinkoff.acquiring.sdk.models.FpsState import ru.tinkoff.acquiring.sdk.models.LoadedState @@ -48,6 +51,7 @@ import ru.tinkoff.acquiring.sdk.models.result.PaymentResult import ru.tinkoff.acquiring.sdk.payment.PaymentListener import ru.tinkoff.acquiring.sdk.payment.PaymentListenerAdapter import ru.tinkoff.acquiring.sdk.payment.PaymentProcess +import ru.tinkoff.acquiring.sdk.payment.pooling.GetStatusPoling import ru.tinkoff.acquiring.sdk.responses.TinkoffPayStatusResponse /** @@ -59,6 +63,7 @@ internal class PaymentViewModel( sdk: AcquiringSdk ) : BaseAcquiringViewModel(application, handleErrorsInSdk, sdk) { + private val getStatusPooling = GetStatusPoling(sdk) private val paymentResult: MutableLiveData = MutableLiveData() private var cardsResult: MutableLiveData> = MutableLiveData() private var tinkoffPayStatusResult: MutableLiveData = MutableLiveData() @@ -66,7 +71,7 @@ internal class PaymentViewModel( private val paymentListener: PaymentListener = createPaymentListener() private val paymentProcess: PaymentProcess = PaymentProcess(sdk, context) - private var requestPaymentStateCount = 0 + private var requestStateJob: Job? = null val paymentResultLiveData: LiveData = paymentResult val cardsResultLiveData: LiveData> = cardsResult @@ -178,42 +183,19 @@ internal class PaymentViewModel( } fun requestPaymentState(paymentId: Long) { - val request = sdk.getState { - this.paymentId = paymentId + requestStateJob?.cancel() + requestStateJob = coroutine.launchOnMain { + getStatusPooling.start(paymentId = paymentId) + .flowOn(Dispatchers.IO) + .catch { handleException(it) } + .filter { ResponseStatus.checkSuccessStatuses(it) } + .collect { handleConfirmOnAuthStatus(paymentId)} } + } - coroutine.call(request, - onSuccess = { response -> - requestPaymentStateCount++ - when (response.status) { - ResponseStatus.CONFIRMED, ResponseStatus.AUTHORIZED -> { - paymentResult.value = PaymentResult(response.paymentId) - requestPaymentStateCount = 0 - changeScreenState(LoadedState) - } - ResponseStatus.FORM_SHOWED -> { - requestPaymentStateCount = 0 - changeScreenState(LoadedState) - changeScreenState(FpsBankFormShowedScreenState(paymentId)) - } - else -> { - if (requestPaymentStateCount == 1) { - changeScreenState(LoadingState) - coroutine.runWithDelay(1000) { - requestPaymentState(paymentId) - } - } else { - changeScreenState(LoadedState) - val throwable = AcquiringSdkException(IllegalStateException("PaymentState = ${response.status}")) - handleException(throwable) - } - } - } - }, - onFailure = { - requestPaymentStateCount = 0 - handleException(it) - }) + private fun handleConfirmOnAuthStatus(paymentId: Long) { + paymentResult.postValue(PaymentResult(paymentId)) + changeScreenState(LoadedState) } private fun createPaymentListener(): PaymentListener { diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt index b9987498..7fc967e0 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt @@ -19,10 +19,9 @@ package ru.tinkoff.acquiring.sdk.viewmodel import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.emvco3ds.sdk.spec.CompletionEvent -import com.emvco3ds.sdk.spec.ProtocolErrorEvent -import com.emvco3ds.sdk.spec.RuntimeErrorEvent -import com.emvco3ds.sdk.spec.Transaction +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* import ru.tinkoff.acquiring.sdk.AcquiringSdk import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.LoadedState @@ -32,15 +31,7 @@ import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus import ru.tinkoff.acquiring.sdk.models.result.AsdkResult import ru.tinkoff.acquiring.sdk.models.result.CardResult import ru.tinkoff.acquiring.sdk.models.result.PaymentResult -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper.cleanupSafe -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusCanceled -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusError -import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusSuccess -import ru.tinkoff.acquiring.sdk.ui.activities.BaseAcquiringActivity -import ru.tinkoff.core.components.threedswrapper.ChallengeStatusReceiverAdapter -import ru.tinkoff.core.components.threedswrapper.ThreeDSWrapper -import ru.tinkoff.core.components.threedswrapper.ThreeDSWrapper.Companion.closeSafe +import ru.tinkoff.acquiring.sdk.payment.pooling.GetStatusPoling internal class ThreeDsViewModel( application: Application, @@ -48,7 +39,9 @@ internal class ThreeDsViewModel( sdk: AcquiringSdk ) : BaseAcquiringViewModel(application, handleErrorsInSdk, sdk) { + private val getStatusPooling = GetStatusPoling(sdk) private val asdkResult: MutableLiveData = MutableLiveData() + private var requestPaymentStateJob: Job? = null val resultLiveData: LiveData = asdkResult fun submitAuthorization(threeDsData: ThreeDsData, transStatus: String) { @@ -73,22 +66,15 @@ internal class ThreeDsViewModel( } fun requestPaymentState(paymentId: Long?) { + requestPaymentStateJob?.cancel() changeScreenState(LoadingState) - - val request = sdk.getState { - this.paymentId = paymentId + requestPaymentStateJob = coroutine.launchOnMain { + getStatusPooling.start(paymentId = paymentId!!) + .flowOn(Dispatchers.IO) + .catch { handleException(it) } + .filter { ResponseStatus.checkSuccessStatuses(it) } + .collect { handleConfirmOnAuthStatus(paymentId)} } - - coroutine.call(request, - onSuccess = { response -> - if (response.status == ResponseStatus.CONFIRMED || response.status == ResponseStatus.AUTHORIZED) { - asdkResult.value = PaymentResult(response.paymentId) - } else { - val throwable = AcquiringSdkException(IllegalStateException("PaymentState = ${response.status}")) - handleException(throwable) - } - changeScreenState(LoadedState) - }) } fun requestAddCardState(requestKey: String?) { @@ -103,10 +89,16 @@ internal class ThreeDsViewModel( if (response.status == ResponseStatus.COMPLETED) { asdkResult.value = CardResult(response.cardId) } else { - val throwable = AcquiringSdkException(IllegalStateException("AsdkState = ${response.status}")) + val throwable = + AcquiringSdkException(IllegalStateException("AsdkState = ${response.status}")) handleException(throwable) } changeScreenState(LoadedState) }) } + + private fun handleConfirmOnAuthStatus(paymentId: Long) { + asdkResult.postValue(PaymentResult(paymentId)) + changeScreenState(LoadedState) + } } \ No newline at end of file diff --git a/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt b/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt new file mode 100644 index 00000000..1cbb31b1 --- /dev/null +++ b/ui/src/test/java/ru/tinkoff/acquiring/sdk/payment/pooling/GetStatusPolingTest.kt @@ -0,0 +1,38 @@ +package ru.tinkoff.acquiring.sdk.payment.pooling + +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Test +import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus + +class GetStatusPolingTest { + + @Test + fun `test when AUTHORIZED`() = runBlocking { + GetStatusPoling { ResponseStatus.AUTHORIZED } + .start(paymentId = 1L) + .collect { println(it) } + } + + @Test + fun `test when REJECTED`() = runBlocking { + val status = ResponseStatus.REJECTED + GetStatusPoling { status } + .start(paymentId = 1L) + .catch { + Assert.assertEquals(it.message, "PaymentState = $status") + } + .collect {} + } + + @Test + fun `test when non terimate status`() = runBlocking { + GetStatusPoling { null } + .start(paymentId = 1L, delayMs = 10) + .catch { + Assert.assertEquals(it.message, "timeout, retries count is over") + } + .collect { println(it) } + } +} \ No newline at end of file From 39f4d9c2b3332ca2bbc763bd28b23a9ef1f85b8e Mon Sep 17 00:00:00 2001 From: jqwout Date: Wed, 29 Mar 2023 23:36:16 +0300 Subject: [PATCH 5/9] EACQAPW-4370 payment id in errors --- .../java/ru/tinkoff/acquiring/sdk/models/ViewState.kt | 2 +- .../acquiring/sdk/ui/activities/BaseAcquiringActivity.kt | 7 ++++++- .../acquiring/sdk/ui/activities/PaymentActivity.kt | 4 ++-- .../acquiring/sdk/ui/activities/SavedCardsActivity.kt | 4 ++-- .../acquiring/sdk/ui/activities/ThreeDsActivity.kt | 2 +- .../acquiring/sdk/ui/activities/TransparentActivity.kt | 3 ++- .../acquiring/sdk/viewmodel/BaseAcquiringViewModel.kt | 8 ++++---- .../tinkoff/acquiring/sdk/viewmodel/PaymentViewModel.kt | 4 ++-- .../tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt | 2 +- .../acquiring/sdk/viewmodel/YandexPaymentViewModel.kt | 2 +- 10 files changed, 22 insertions(+), 16 deletions(-) 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 f16ab7c6..1aff2c02 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 @@ -25,7 +25,7 @@ internal sealed class ScreenState internal object DefaultScreenState : ScreenState() internal class ErrorScreenState(val message: String) : ScreenState() -internal class FinishWithErrorScreenState(val error: Throwable) : ScreenState() +internal class FinishWithErrorScreenState(val error: Throwable, val paymentId: Long? = null) : ScreenState() internal class FpsBankFormShowedScreenState(val paymentId: Long) : ScreenState() internal sealed class Screen : ScreenState() diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt index fa8ee66a..ef406639 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt @@ -179,7 +179,8 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { finish() } - open fun finishWithError(throwable: Throwable) { + open fun finishWithError(throwable: Throwable, paymentId: Long? = null) { + setPaymentIdToExtra(paymentId) setErrorResult(throwable) finish() } @@ -207,4 +208,8 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { } }) } + + protected fun setPaymentIdToExtra(paymentId: Long? = null) { + intent.putExtra(TinkoffAcquiring.EXTRA_PAYMENT_ID, paymentId) + } } \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/PaymentActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/PaymentActivity.kt index cc07e0e7..cee9d0d4 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/PaymentActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/PaymentActivity.kt @@ -179,7 +179,7 @@ internal class PaymentActivity : TransparentActivity() { ThreeDsHelper.Launch(this@PaymentActivity, THREE_DS_REQUEST_CODE, options, screen.data, screen.transaction) } catch (e: Throwable) { - finishWithError(e) + finishWithError(e, screen.data.paymentId) } } is BrowseFpsBankScreenState -> openBankChooser(screen.deepLink, screen.banks) @@ -192,7 +192,7 @@ internal class PaymentActivity : TransparentActivity() { private fun handleScreenState(screenState: ScreenState) { when (screenState) { - is FinishWithErrorScreenState -> finishWithError(screenState.error) + is FinishWithErrorScreenState -> finishWithError(screenState.error, screenState.paymentId) is ErrorScreenState -> { if (asdkState is FpsState || asdkState is BrowseFpsBankState || asdkState is OpenTinkoffPayBankState) { finishWithError(AcquiringSdkException(NetworkException(screenState.message))) diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt index 28e83b71..74d6e9dc 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt @@ -145,9 +145,9 @@ internal class SavedCardsActivity : BaseAcquiringActivity(), CardListAdapter.OnM notificationDialog?.dismiss() } - override fun finishWithError(throwable: Throwable) { + override fun finishWithError(throwable: Throwable, paymentId: Long?) { isErrorOccurred = true - super.finishWithError(throwable) + super.finishWithError(throwable, null) } override fun finish() { 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 b2e5d264..b2b27335 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 @@ -121,7 +121,7 @@ internal class ThreeDsActivity : BaseAcquiringActivity() { private fun handleScreenState(screenState: ScreenState) { when (screenState) { is ErrorScreenState -> finishWithError(AcquiringSdkException(IllegalStateException(screenState.message))) - is FinishWithErrorScreenState -> finishWithError(screenState.error) + is FinishWithErrorScreenState -> finishWithError(screenState.error, screenState.paymentId) } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt index 689070c6..c4ae5335 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt @@ -137,7 +137,8 @@ internal open class TransparentActivity : BaseAcquiringActivity() { closeActivity() } - override fun finishWithError(throwable: Throwable) { + override fun finishWithError(throwable: Throwable, paymentId: Long?) { + setPaymentIdToExtra(paymentId) setErrorResult(throwable) closeActivity() } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/BaseAcquiringViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/BaseAcquiringViewModel.kt index 3289af4c..b5134ae5 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/BaseAcquiringViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/BaseAcquiringViewModel.kt @@ -56,7 +56,7 @@ internal open class BaseAcquiringViewModel( coroutine.cancelAll() } - fun handleException(throwable: Throwable) { + fun handleException(throwable: Throwable, paymentId: Long? = null) { loadState.value = LoadedState when (throwable) { is NetworkException -> changeScreenState(ErrorScreenState(AsdkLocalization.resources.payDialogErrorNetwork!!)) @@ -66,10 +66,10 @@ internal open class BaseAcquiringViewModel( if (errorCode != null && (AcquiringApi.errorCodesFallback.contains(errorCode) || AcquiringApi.errorCodesForUserShowing.contains(errorCode))) { changeScreenState(ErrorScreenState(resolveErrorMessage(throwable))) - } else changeScreenState(FinishWithErrorScreenState(throwable)) - } else changeScreenState(FinishWithErrorScreenState(throwable)) + } else changeScreenState(FinishWithErrorScreenState(throwable, paymentId)) + } else changeScreenState(FinishWithErrorScreenState(throwable, paymentId)) } - else -> changeScreenState(FinishWithErrorScreenState(throwable)) + else -> changeScreenState(FinishWithErrorScreenState(throwable,paymentId)) } } 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 dc9dcda5..8cfbb391 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 @@ -187,7 +187,7 @@ internal class PaymentViewModel( requestStateJob = coroutine.launchOnMain { getStatusPooling.start(paymentId = paymentId) .flowOn(Dispatchers.IO) - .catch { handleException(it) } + .catch { handleException(it, paymentId) } .filter { ResponseStatus.checkSuccessStatuses(it) } .collect { handleConfirmOnAuthStatus(paymentId)} } @@ -231,7 +231,7 @@ internal class PaymentViewModel( override fun onError(throwable: Throwable, paymentId: Long?) { changeScreenState(LoadedState) - handleException(throwable) + handleException(throwable, paymentId) } } } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt index 7fc967e0..e2854aff 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/ThreeDsViewModel.kt @@ -71,7 +71,7 @@ internal class ThreeDsViewModel( requestPaymentStateJob = coroutine.launchOnMain { getStatusPooling.start(paymentId = paymentId!!) .flowOn(Dispatchers.IO) - .catch { handleException(it) } + .catch { handleException(it, paymentId) } .filter { ResponseStatus.checkSuccessStatuses(it) } .collect { handleConfirmOnAuthStatus(paymentId)} } 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 index 58b43ee4..1273fcc8 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/YandexPaymentViewModel.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/viewmodel/YandexPaymentViewModel.kt @@ -86,7 +86,7 @@ internal class YandexPaymentViewModel( } is YandexPaymentState.Error -> { changeScreenState(LoadedState) - handleException(it.throwable) + handleException(it.throwable, it.paymentId) } else -> Unit } From 25a5ddf15ba4b6d1844d32c0ceaf43d95cca79d2 Mon Sep 17 00:00:00 2001 From: jqwout Date: Thu, 30 Mar 2023 12:40:40 +0300 Subject: [PATCH 6/9] =?UTF-8?q?EACQAPW-4370=20-=20=D0=BF=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8=D0=B4=D1=8B=D0=B2=D0=B0=D0=B5=D0=BC=20paymentId=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B5,=20=D0=B5?= =?UTF-8?q?=D1=81=D0=BB=D0=B8=20=D0=BE=D0=BD=20=D0=B1=D1=8B=D0=BB=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/sample/ui/PayableActivity.kt | 28 ++++++++++++------- .../ui/activities/BaseAcquiringActivity.kt | 10 ++----- .../sdk/ui/activities/SavedCardsActivity.kt | 2 +- .../sdk/ui/activities/ThreeDsActivity.kt | 4 ++- .../sdk/ui/activities/TransparentActivity.kt | 12 ++++++-- .../tinkoff/acquiring/sdk/utils/IntentExt.kt | 10 +++++++ 6 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/IntentExt.kt diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index 1efa9b7f..ccc41b9d 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -42,6 +42,7 @@ import ru.tinkoff.acquiring.sdk.payment.PaymentListener import ru.tinkoff.acquiring.sdk.payment.PaymentListenerAdapter import ru.tinkoff.acquiring.sdk.payment.PaymentState import ru.tinkoff.acquiring.sdk.utils.Money +import ru.tinkoff.acquiring.sdk.utils.getLongOrNull import ru.tinkoff.acquiring.yandexpay.YandexButtonFragment import ru.tinkoff.acquiring.yandexpay.addYandexResultListener import ru.tinkoff.acquiring.yandexpay.createYandexPayButtonFragment @@ -268,11 +269,7 @@ open class PayableActivity : AppCompatActivity() { RESULT_OK -> onSuccessPayment() RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { - Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - getErrorFromIntent(data)?.run { - printStackTrace() - logIfTimeout() - } + commonErrorHandler(data) } } } @@ -284,11 +281,7 @@ open class PayableActivity : AppCompatActivity() { } RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() RESULT_ERROR -> { - Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - getErrorFromIntent(data)?.run { - printStackTrace() - logIfTimeout() - } + commonErrorHandler(data) } } } @@ -362,6 +355,16 @@ open class PayableActivity : AppCompatActivity() { isProgressShowing = false } + + private fun commonErrorHandler(data: Intent?) { + Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() + data?.logPaymentId() + getErrorFromIntent(data)?.run { + printStackTrace() + logIfTimeout() + } + } + private fun getErrorFromIntent(data: Intent?): Throwable? { return (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable) } @@ -377,6 +380,11 @@ open class PayableActivity : AppCompatActivity() { } } + private fun Intent.logPaymentId() { + val id = getLongOrNull(TinkoffAcquiring.EXTRA_PAYMENT_ID) + log("paymentId : $id") + } + companion object { const val PAYMENT_REQUEST_CODE = 1 diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt index ef406639..bf871943 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt @@ -168,9 +168,10 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { setResult(Activity.RESULT_OK, intent) } - protected open fun setErrorResult(throwable: Throwable) { + protected open fun setErrorResult(throwable: Throwable, paymentId: Long? = null) { val intent = Intent() intent.putExtra(TinkoffAcquiring.EXTRA_ERROR, throwable) + intent.putExtra(TinkoffAcquiring.EXTRA_PAYMENT_ID, paymentId) setResult(TinkoffAcquiring.RESULT_ERROR, intent) } @@ -180,8 +181,7 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { } open fun finishWithError(throwable: Throwable, paymentId: Long? = null) { - setPaymentIdToExtra(paymentId) - setErrorResult(throwable) + setErrorResult(throwable, paymentId) finish() } @@ -208,8 +208,4 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { } }) } - - protected fun setPaymentIdToExtra(paymentId: Long? = null) { - intent.putExtra(TinkoffAcquiring.EXTRA_PAYMENT_ID, paymentId) - } } \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt index 74d6e9dc..928b4a02 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/SavedCardsActivity.kt @@ -175,7 +175,7 @@ internal class SavedCardsActivity : BaseAcquiringActivity(), CardListAdapter.OnM setResult(Activity.RESULT_OK, intent) } - override fun setErrorResult(throwable: Throwable) { + override fun setErrorResult(throwable: Throwable,paymentId: Long?) { val intent = Intent() intent.putExtra(TinkoffAcquiring.EXTRA_ERROR, throwable) intent.putExtra(TinkoffAcquiring.EXTRA_CARD_ID, selectedCardId) 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 b2b27335..0c35f3c8 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 @@ -29,6 +29,7 @@ import android.webkit.WebViewClient import androidx.lifecycle.Observer import org.json.JSONObject import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.TinkoffAcquiring import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.models.ErrorScreenState import ru.tinkoff.acquiring.sdk.models.FinishWithErrorScreenState @@ -104,8 +105,9 @@ internal class ThreeDsActivity : BaseAcquiringActivity() { setResult(Activity.RESULT_OK, intent) } - override fun setErrorResult(throwable: Throwable) { + override fun setErrorResult(throwable: Throwable, paymentId: Long?) { val intent = Intent() + intent.putExtra(TinkoffAcquiring.EXTRA_PAYMENT_ID, paymentId) intent.putExtra(ThreeDsHelper.Launch.ERROR_DATA, throwable) setResult(ThreeDsHelper.Launch.RESULT_ERROR, intent) } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt index c4ae5335..b68ca0c3 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/TransparentActivity.kt @@ -26,6 +26,7 @@ import android.view.View import android.view.WindowManager import androidx.appcompat.widget.Toolbar import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.TinkoffAcquiring import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkException import ru.tinkoff.acquiring.sdk.localization.AsdkLocalization import ru.tinkoff.acquiring.sdk.localization.LocalizationResources @@ -41,6 +42,8 @@ import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusCanceled import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusError import ru.tinkoff.acquiring.sdk.threeds.ThreeDsStatusSuccess import ru.tinkoff.acquiring.sdk.ui.customview.BottomContainer +import ru.tinkoff.acquiring.sdk.utils.getAsError +import ru.tinkoff.acquiring.sdk.utils.getLongOrNull import ru.tinkoff.acquiring.sdk.viewmodel.ThreeDsViewModel /** @@ -104,7 +107,11 @@ internal open class TransparentActivity : BaseAcquiringActivity() { if (resultCode == Activity.RESULT_OK && data != null) { finishWithSuccess(data.getSerializableExtra(ThreeDsHelper.Launch.RESULT_DATA) as AsdkResult) } else if (resultCode == ThreeDsHelper.Launch.RESULT_ERROR) { - finishWithError(data?.getSerializableExtra(ThreeDsHelper.Launch.ERROR_DATA) as Throwable) + checkNotNull(data) + finishWithError( + data.getAsError(ThreeDsHelper.Launch.ERROR_DATA), + data.getLongOrNull(TinkoffAcquiring.EXTRA_PAYMENT_ID) + ) } else { setResult(Activity.RESULT_CANCELED) closeActivity() @@ -138,8 +145,7 @@ internal open class TransparentActivity : BaseAcquiringActivity() { } override fun finishWithError(throwable: Throwable, paymentId: Long?) { - setPaymentIdToExtra(paymentId) - setErrorResult(throwable) + setErrorResult(throwable, paymentId) closeActivity() } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/IntentExt.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/IntentExt.kt new file mode 100644 index 00000000..f952118a --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/utils/IntentExt.kt @@ -0,0 +1,10 @@ +package ru.tinkoff.acquiring.sdk.utils + +import android.content.Intent + +/** + * Created by i.golovachev + */ +fun Intent.getAsError(key: String) = getSerializableExtra(key) as Throwable + +fun Intent.getLongOrNull(key: String) : Long? = getLongExtra(key,-1).takeIf { it > -1 } \ No newline at end of file From 63085b0b470bf90e868320ad038b866aeee578ae Mon Sep 17 00:00:00 2001 From: jqwout Date: Thu, 30 Mar 2023 15:50:23 +0300 Subject: [PATCH 7/9] =?UTF-8?q?EACQAPW-4370=20-=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=82=D0=BE=D1=81=D1=82=20,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=20=D0=B1=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=82=D1=80=D0=B0=D0=BD=D1=81?= =?UTF-8?q?=D0=BB=D1=8F=D1=86=D0=B8=D1=8E=20paymentId=20=D0=B2=20=D0=BE?= =?UTF-8?q?=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/sample/ui/PayableActivity.kt | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index ccc41b9d..0657dae5 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -24,6 +24,7 @@ import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.commit import ru.tinkoff.acquiring.sample.R @@ -32,6 +33,7 @@ import ru.tinkoff.acquiring.sample.utils.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager import ru.tinkoff.acquiring.sdk.AcquiringSdk.Companion.log import ru.tinkoff.acquiring.sdk.TinkoffAcquiring +import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.EXTRA_PAYMENT_ID import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.RESULT_ERROR import ru.tinkoff.acquiring.sdk.exceptions.AcquiringSdkTimeoutException import ru.tinkoff.acquiring.sdk.localization.AsdkSource @@ -357,34 +359,31 @@ open class PayableActivity : AppCompatActivity() { private fun commonErrorHandler(data: Intent?) { - Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - data?.logPaymentId() - getErrorFromIntent(data)?.run { - printStackTrace() - logIfTimeout() - } + val error = getErrorFromIntent(data) + val paymentIdFromIntent = data?.getLongOrNull(EXTRA_PAYMENT_ID) + val message = configureToastMessage(error, paymentIdFromIntent) + log("toast message: $message") + error?.printStackTrace() + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } private fun getErrorFromIntent(data: Intent?): Throwable? { return (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable) } - private fun Throwable.logIfTimeout() { - (this as? AcquiringSdkTimeoutException)?.run { - if (paymentId != null) { - log("paymentId : $paymentId") - } - if (status != null) { - log("status : $status") - } + private fun configureToastMessage(error: Throwable?, paymentId: Long?): String { + val acqSdkTimeout = error as? AcquiringSdkTimeoutException + val payment = paymentId ?: acqSdkTimeout?.paymentId + val status = acqSdkTimeout?.status + return buildString { + append(getString(R.string.payment_failed)) + append(" ") + payment?.let { append("paymentId: $it") } + append(" ") + status?.let { append("status: $it") } } } - private fun Intent.logPaymentId() { - val id = getLongOrNull(TinkoffAcquiring.EXTRA_PAYMENT_ID) - log("paymentId : $id") - } - companion object { const val PAYMENT_REQUEST_CODE = 1 From cb63a3c0a37bb40c8d5c2782b3f397fefa3c0956 Mon Sep 17 00:00:00 2001 From: merkost Date: Mon, 3 Apr 2023 11:29:17 +1000 Subject: [PATCH 8/9] Added Activity Result API support with example --- .../acquiring/sample/ui/DetailsActivity.kt | 33 +++++++++++++++++- .../acquiring/sample/ui/PayableActivity.kt | 5 +++ .../tinkoff/acquiring/sdk/TinkoffAcquiring.kt | 34 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt index 4f60ca1e..9e014766 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt @@ -25,6 +25,9 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -34,6 +37,7 @@ import ru.tinkoff.acquiring.sample.models.Book import ru.tinkoff.acquiring.sample.models.BooksRegistry import ru.tinkoff.acquiring.sample.models.Cart import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.TinkoffAcquiring import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions import ru.tinkoff.acquiring.sdk.payment.PaymentProcess.Companion.configure import ru.tinkoff.acquiring.yandexpay.models.YandexPayData @@ -55,6 +59,23 @@ class DetailsActivity : PayableActivity() { private var book: Book? = null + private val paymentContract = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult -> + when (result.resultCode) { + RESULT_OK -> { + Toast.makeText(this, + R.string.notification_payment_success, + Toast.LENGTH_SHORT).show() + } + RESULT_CANCELED -> Toast.makeText(this, + R.string.payment_cancelled, + Toast.LENGTH_SHORT).show() + TinkoffAcquiring.RESULT_ERROR -> Toast.makeText(this, + R.string.payment_failed, + Toast.LENGTH_SHORT).show() + } + } + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -84,7 +105,11 @@ class DetailsActivity : PayableActivity() { val buttonBuy = findViewById(R.id.btn_buy_now) buttonBuy.setOnClickListener { - initPayment() + //Стандартный метод проведения оплаты с получением результата в OnActivityResult + //initPayment() + + //Метод проведения оплаты с получением результата в ActivityResultAPI + initActivityResultAPIPayment() } val sbpButton = findViewById(R.id.btn_fps_pay) @@ -100,6 +125,12 @@ class DetailsActivity : PayableActivity() { fillViews() } + private fun initActivityResultAPIPayment() { + val pendingIntent = getPaymentPendingIntent() + val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build() + paymentContract.launch(intentSenderRequest) + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.details_menu, menu) diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index 92aeae45..6c6700ae 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -18,6 +18,7 @@ package ru.tinkoff.acquiring.sample.ui import android.annotation.SuppressLint import android.app.AlertDialog +import android.app.PendingIntent import android.content.Intent import android.os.Bundle import android.view.MenuItem @@ -138,6 +139,10 @@ open class PayableActivity : AppCompatActivity() { tinkoffAcquiring.openPaymentScreen(this, createPaymentOptions(), PAYMENT_REQUEST_CODE) } + protected fun getPaymentPendingIntent(): PendingIntent { + return tinkoffAcquiring.getPaymentPendingIntent(this, createPaymentOptions(), PAYMENT_REQUEST_CODE) + } + protected fun openDynamicQrScreen() { tinkoffAcquiring.openDynamicQrScreen(this, createPaymentOptions(), DYNAMIC_QR_PAYMENT_REQUEST_CODE) } diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/TinkoffAcquiring.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/TinkoffAcquiring.kt index 996af633..4ffc3759 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/TinkoffAcquiring.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/TinkoffAcquiring.kt @@ -20,6 +20,7 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.Build import androidx.fragment.app.Fragment import kotlinx.coroutines.* import ru.tinkoff.acquiring.sdk.localization.LocalizationSource @@ -120,6 +121,39 @@ class TinkoffAcquiring( return PaymentProcess(sdk, applicationContext).createFinishProcess(paymentId, paymentSource) } + /** + * Получение Acquiring SDK PendingIntent для проведения оплаты и получения результата с помощью Activity Result API + * + * @param activity контекст для запуска экрана из Activity + * @param options настройки платежной сессии и визуального отображения экрана + * @param requestCode код для получения результата, по завершению оплаты + * @param state вспомогательный параметр для запуска экрана Acquiring SDK + * с заданного состояния + * @return настроенный PendingIntent + */ + @JvmOverloads + fun getPaymentPendingIntent( + activity: Activity, + options: PaymentOptions, + requestCode: Int, + state: AsdkState = DefaultState + ): PendingIntent { + options.asdkState = state + val intent = prepareIntent(activity, options, PaymentActivity::class.java) + + val flags = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> PendingIntent.FLAG_UPDATE_CURRENT + else -> PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } + + return PendingIntent.getActivity( + activity, + requestCode, + intent, + flags + ) + } + /** * Запуск экрана Acquiring SDK для проведения оплаты * From 8885561b54e72757a5e8c654b98c2d7dce998f7f Mon Sep 17 00:00:00 2001 From: jqwout Date: Mon, 3 Apr 2023 15:33:38 +0300 Subject: [PATCH 9/9] =?UTF-8?q?EACQAPW-4401=5Fapi=5Fresult=5Fusage=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=81=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tinkoff/acquiring/sample/ui/DetailsActivity.kt | 14 +------------- .../tinkoff/acquiring/sample/ui/PayableActivity.kt | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt index 9e014766..c1670cab 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt @@ -61,19 +61,7 @@ class DetailsActivity : PayableActivity() { private val paymentContract = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult -> - when (result.resultCode) { - RESULT_OK -> { - Toast.makeText(this, - R.string.notification_payment_success, - Toast.LENGTH_SHORT).show() - } - RESULT_CANCELED -> Toast.makeText(this, - R.string.payment_cancelled, - Toast.LENGTH_SHORT).show() - TinkoffAcquiring.RESULT_ERROR -> Toast.makeText(this, - R.string.payment_failed, - Toast.LENGTH_SHORT).show() - } + handlePaymentResult(result.resultCode, result.data) } public override fun onCreate(savedInstanceState: Bundle?) { diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index c6f3d317..fc8f0b35 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt @@ -271,7 +271,7 @@ open class PayableActivity : AppCompatActivity() { } } - private fun handlePaymentResult(resultCode: Int, data: Intent?) { + protected fun handlePaymentResult(resultCode: Int, data: Intent?) { when (resultCode) { RESULT_OK -> onSuccessPayment() RESULT_CANCELED -> Toast.makeText(this, R.string.payment_cancelled, Toast.LENGTH_SHORT).show() @@ -281,7 +281,7 @@ open class PayableActivity : AppCompatActivity() { } } - private fun handleYandexPayResult(resultCode: Int, data: Intent?) { + protected fun handleYandexPayResult(resultCode: Int, data: Intent?) { when (resultCode) { RESULT_OK -> { acqFragment?.options = createPaymentOptions()