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/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/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/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/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/DetailsActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/DetailsActivity.kt index 4f60ca1e..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 @@ -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,11 @@ class DetailsActivity : PayableActivity() { private var book: Book? = null + private val paymentContract = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult -> + handlePaymentResult(result.resultCode, result.data) + } + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -84,7 +93,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 +113,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/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/PayableActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/PayableActivity.kt index 92aeae45..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 @@ -18,31 +18,34 @@ 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 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 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.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 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.sdk.utils.getLongOrNull import ru.tinkoff.acquiring.yandexpay.YandexButtonFragment import ru.tinkoff.acquiring.yandexpay.addYandexResultListener import ru.tinkoff.acquiring.yandexpay.createYandexPayButtonFragment @@ -138,6 +141,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) } @@ -264,26 +271,24 @@ 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() RESULT_ERROR -> { - Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() - (data?.getSerializableExtra(TinkoffAcquiring.EXTRA_ERROR) as? Throwable)?.printStackTrace() + commonErrorHandler(data) } } } - private fun handleYandexPayResult(resultCode: Int, data: Intent?) { + protected fun handleYandexPayResult(resultCode: Int, data: Intent?) { when (resultCode) { RESULT_OK -> { acqFragment?.options = createPaymentOptions() } 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() + commonErrorHandler(data) } } } @@ -357,6 +362,33 @@ open class PayableActivity : AppCompatActivity() { isProgressShowing = false } + + private fun commonErrorHandler(data: Intent?) { + 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 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") } + } + } + companion object { const val PAYMENT_REQUEST_CODE = 1 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/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 new file mode 100644 index 00000000..ea61f4b8 --- /dev/null +++ b/sample/src/main/res/layout/asdk_environment_dialog.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/menu/main_menu.xml b/sample/src/main/res/menu/main_menu.xml index ff0b8bfb..45f4ee05 100644 --- a/sample/src/main/res/menu/main_menu.xml +++ b/sample/src/main/res/menu/main_menu.xml @@ -60,6 +60,11 @@ android:title="@string/activity_title_terminals" app:showAsAction="never"/> + + Привязать карту вручную Терминалы О программе + Окружение Открыть статический 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 @@ - - - + + + + + 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 для проведения оплаты * 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/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/ui/activities/BaseAcquiringActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/ui/activities/BaseAcquiringActivity.kt index fa8ee66a..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) } @@ -179,8 +180,8 @@ internal open class BaseAcquiringActivity : AppCompatActivity() { finish() } - open fun finishWithError(throwable: Throwable) { - setErrorResult(throwable) + open fun finishWithError(throwable: Throwable, paymentId: Long? = null) { + setErrorResult(throwable, paymentId) finish() } 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..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 @@ -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() { @@ -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 b2e5d264..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) } @@ -121,7 +123,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..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() @@ -137,8 +144,8 @@ internal open class TransparentActivity : BaseAcquiringActivity() { closeActivity() } - override fun finishWithError(throwable: Throwable) { - setErrorResult(throwable) + override fun finishWithError(throwable: Throwable, paymentId: Long?) { + setErrorResult(throwable, paymentId) closeActivity() } 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/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 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 f5b16d76..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 @@ -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, paymentId) } + .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 { @@ -249,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 b9987498..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 @@ -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, paymentId) } + .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/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 } 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