diff --git a/gradle.properties b/gradle.properties index 0aedba5..cd403f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,4 +19,7 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -org.gradle.unsafe.configuration-cache = true \ No newline at end of file +org.gradle.unsafe.configuration-cache = true +android.defaults.buildfeatures.buildconfig = true +android.nonTransitiveRClass = false +android.nonFinalResIds = false \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApi.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApi.kt index 4cc874c..fd32f45 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApi.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApi.kt @@ -20,6 +20,11 @@ class CloudpaymentsApi @Inject constructor(private val apiService: Cloudpayments .subscribeOn(Schedulers.io()) } + fun getMerchantConfiguration(publicId: String): Single { + return apiService.getMerchantConfiguration(publicId) + .subscribeOn(Schedulers.io()) + } + fun charge(requestBody: PaymentRequestBody): Single { return apiService.charge(requestBody) .subscribeOn(Schedulers.io()) @@ -58,6 +63,16 @@ class CloudpaymentsApi @Inject constructor(private val apiService: Cloudpayments } } + fun getTinkoffPayQrLink(requestBody: TinkoffPayQrLinkBody): Single { + return apiService.getTinkoffPayQrLink(requestBody) + .subscribeOn(Schedulers.io()) + } + + fun qrLinkStatusWait(requestBody: QrLinkStatusWaitBody): Single { + return apiService.qrLinkStatusWait(requestBody) + .subscribeOn(Schedulers.io()) + } + fun getBinInfo(firstSixDigits: String): Single = if (firstSixDigits.length < 6) { Single.error(CloudpaymentsTransactionError("You must specify the first 6 digits of the card number")) diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApiService.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApiService.kt index 5b4fcae..7ab4aa1 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApiService.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/CloudpaymentsApiService.kt @@ -5,11 +5,17 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query import ru.cloudpayments.sdk.api.models.CloudpaymentsBinInfoResponse +import ru.cloudpayments.sdk.api.models.CloudpaymentsGetTinkoffPayQrLinkResponse +import ru.cloudpayments.sdk.api.models.CloudpaymentsMerchantConfigurationResponse import ru.cloudpayments.sdk.api.models.CloudpaymentsPublicKeyResponse import ru.cloudpayments.sdk.api.models.PaymentRequestBody import ru.cloudpayments.sdk.api.models.ThreeDsRequestBody import ru.cloudpayments.sdk.api.models.CloudpaymentsTransactionResponse +import ru.cloudpayments.sdk.api.models.QrLinkStatusWaitBody +import ru.cloudpayments.sdk.api.models.QrLinkStatusWaitResponse +import ru.cloudpayments.sdk.api.models.TinkoffPayQrLinkBody interface CloudpaymentsApiService { @POST("payments/cards/charge") @@ -26,4 +32,13 @@ interface CloudpaymentsApiService { @GET("payments/publickey") fun getPublicKey(): Single + + @GET("merchant/configuration") + fun getMerchantConfiguration(@Query("terminalPublicId") publicId: String): Single + + @POST("payments/qr/tinkoffpay/link") + fun getTinkoffPayQrLink(@Body body: TinkoffPayQrLinkBody): Single + + @POST("payments/qr/status/wait") + fun qrLinkStatusWait(@Body body: QrLinkStatusWaitBody): Single } \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsGetTinkoffPayQrLinkResponse.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsGetTinkoffPayQrLinkResponse.kt new file mode 100644 index 0000000..80449f7 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsGetTinkoffPayQrLinkResponse.kt @@ -0,0 +1,17 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName +import io.reactivex.Observable + +data class CloudpaymentsGetTinkoffPayQrLinkResponse( + @SerializedName("Success") val success: Boolean?, + @SerializedName("Message") val message: String?, + @SerializedName("Model") val transaction: CloudpaymentsTinkoffPayQrLinkTransaction?) { + fun handleError(): Observable { + return if (success == true ) { + Observable.just(this) + } else { + Observable.error(CloudpaymentsTransactionError(message ?: "")) + } + } +} \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsMerchantConfigurationResponse.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsMerchantConfigurationResponse.kt new file mode 100644 index 0000000..5a6144d --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsMerchantConfigurationResponse.kt @@ -0,0 +1,23 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName + +data class CloudpaymentsMerchantConfigurationResponse( + @SerializedName("Success") val success: Boolean?, + @SerializedName("Message") val message: String?, + @SerializedName("Model") val model: MerchantConfiguration? +) + +data class MerchantConfiguration( + @SerializedName("ExternalPaymentMethods") val externalPaymentMethods: ArrayList?, + @SerializedName("Features") val features: Features? +) + +data class ExternalPaymentMethods( + @SerializedName("Type") val type: Int?, + @SerializedName("Enabled") val enabled: Boolean? +) + +data class Features( + @SerializedName("IsSaveCard") val isSaveCard: Int? +) \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsTinkoffPayQrLinkTransaction.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsTinkoffPayQrLinkTransaction.kt new file mode 100644 index 0000000..43e75d1 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/CloudpaymentsTinkoffPayQrLinkTransaction.kt @@ -0,0 +1,8 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName + +data class CloudpaymentsTinkoffPayQrLinkTransaction( + @SerializedName("TransactionId") val transactionId: Int?, + @SerializedName("ProviderQrId") val providerQrId: String?, + @SerializedName("QrUrl") val qrUrl: String?) \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWait.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWait.kt new file mode 100644 index 0000000..2139721 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWait.kt @@ -0,0 +1,10 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName + +data class QrLinkStatusWait( + @SerializedName("TransactionId") val transactionId: Int?, + @SerializedName("Status") val status: String?, + @SerializedName("StatusCode") val statusCode: String?) + + diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitBody.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitBody.kt new file mode 100644 index 0000000..bc12e6d --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitBody.kt @@ -0,0 +1,6 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName + +data class QrLinkStatusWaitBody( + @SerializedName("TransactionId") val transactionId: Int) \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitResponse.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitResponse.kt new file mode 100644 index 0000000..7d34f17 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/QrLinkStatusWaitResponse.kt @@ -0,0 +1,17 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName +import io.reactivex.Observable + +data class QrLinkStatusWaitResponse( + @SerializedName("Success") val success: Boolean?, + @SerializedName("Message") val message: String?, + @SerializedName("Model") val transaction: QrLinkStatusWait?) { + fun handleError(): Observable { + return if (success == true ){ + Observable.just(this) + } else { + Observable.error(CloudpaymentsTransactionError(message ?: "")) + } + } +} \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/api/models/TinkoffPayQrLinkBody.kt b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/TinkoffPayQrLinkBody.kt new file mode 100644 index 0000000..086a416 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/api/models/TinkoffPayQrLinkBody.kt @@ -0,0 +1,19 @@ +package ru.cloudpayments.sdk.api.models + +import com.google.gson.annotations.SerializedName + +data class TinkoffPayQrLinkBody( + @SerializedName("Webview") val webView: Boolean = true, // Мобильное устройство + @SerializedName("Device") val device: String = "MobileApp", // Вызов из мобильных приложений + @SerializedName("Amount") val amount: String, // Сумма + @SerializedName("Currency") val currency: String, // Валюта + @SerializedName("Description") val description: String? = null, // Описание платежа + @SerializedName("AccountId") val accountId: String? = null, // Identity плательщика в системе мерчанта + @SerializedName("Email") val email: String? = null, // E-mail плательщика + @SerializedName("JsonData") val jsonData: String? = null, // Произвольные данные мерчанта в формате JSON + @SerializedName("InvoiceId") val invoiceId: String? = null, // id заказа в системе мерчанта + @SerializedName("Scheme") val scheme: String, // charge - одностадийная оплата, auth - двухстадийная оплата (Scheme":"0") + @SerializedName("TtlMinutes") val ttlMinutes: Int = 30, // Время жизни Qr + @SerializedName("SuccessRedirectUrl") val successRedirectUrl: String = "https://cp.ru", // Url успешной оплаты (мерчанта) + @SerializedName("FailRedirectUrl") val failRedirectUrl: String = "https://cp.ru", // Url неуспешной оплаты (мерчанта) + @SerializedName("SaveCard") var saveCard: Boolean? = null) \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/configuration/PaymentData.kt b/sdk/src/main/java/ru/cloudpayments/sdk/configuration/PaymentData.kt index 2151468..7eed435 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/configuration/PaymentData.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/configuration/PaymentData.kt @@ -1,15 +1,57 @@ package ru.cloudpayments.sdk.configuration import android.os.Parcelable +import android.util.Log +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException +import com.google.gson.annotations.SerializedName import kotlinx.android.parcel.Parcelize +import ru.cloudpayments.sdk.Constants import ru.cloudpayments.sdk.api.models.PaymentDataPayer +import ru.cloudpayments.sdk.util.TAG @Parcelize -class PaymentData(val amount: String, - var currency: String = "RUB", - val invoiceId: String? = null, - val description: String? = null, - val accountId: String? = null, - var email: String? = null, - val payer: PaymentDataPayer? = null, - val jsonData: String? = null): Parcelable \ No newline at end of file +class PaymentData( + val amount: String, + var currency: String = "RUB", + val invoiceId: String? = null, + val description: String? = null, + val accountId: String? = null, + var email: String? = null, + val payer: PaymentDataPayer? = null, + val jsonData: String? = null +) : Parcelable { + + fun jsonDataHasRecurrent(): Boolean { + + if (!jsonData.isNullOrEmpty()) { + val gson = GsonBuilder() + .setLenient() + .create() + + try { + val cpJsonData = gson.fromJson(jsonData, CpJsonData::class.java) + cpJsonData.cloudPayments?.recurrent?.interval?.let { + return true + } + } catch (e: JsonSyntaxException) { + Log.e(TAG, "JsonData syntax error") + } + } + return false + } +} + +data class CpJsonData( + @SerializedName("cloudPayments") val cloudPayments: CloudPaymentsJsonData? +) + +data class CloudPaymentsJsonData( + @SerializedName("recurrent") val recurrent: CloudPaymentsRecurrentJsonData? +) + +data class CloudPaymentsRecurrentJsonData( + @SerializedName("interval") val interval: String?, + @SerializedName("period") val period: String?, + @SerializedName("amount") val amount: String? +) diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/dagger2/CloudpaymentsModule.kt b/sdk/src/main/java/ru/cloudpayments/sdk/dagger2/CloudpaymentsModule.kt index 4b27999..cab613d 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/dagger2/CloudpaymentsModule.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/dagger2/CloudpaymentsModule.kt @@ -50,8 +50,8 @@ class CloudpaymentsNetModule(private val publicId: String, private var apiUrl: S authenticationInterceptor: AuthenticationInterceptor): CloudpaymentsApiService { val client = okHttpClientBuilder .addInterceptor(authenticationInterceptor) - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(20, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) .followRedirects(false) .build() diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/models/PayParams.kt b/sdk/src/main/java/ru/cloudpayments/sdk/models/PayParams.kt new file mode 100644 index 0000000..fd77443 --- /dev/null +++ b/sdk/src/main/java/ru/cloudpayments/sdk/models/PayParams.kt @@ -0,0 +1,5 @@ +package ru.cloudpayments.sdk.models + +data class PayParams( + var saveCard: Boolean? = null + ) \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/ui/PaymentActivity.kt b/sdk/src/main/java/ru/cloudpayments/sdk/ui/PaymentActivity.kt index dbd7ea1..7df79a7 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/ui/PaymentActivity.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/ui/PaymentActivity.kt @@ -24,6 +24,7 @@ import ru.cloudpayments.sdk.dagger2.CloudpaymentsModule import ru.cloudpayments.sdk.dagger2.CloudpaymentsNetModule import ru.cloudpayments.sdk.dagger2.DaggerCloudpaymentsComponent import ru.cloudpayments.sdk.databinding.ActivityCpsdkPaymentBinding +import ru.cloudpayments.sdk.models.PayParams import ru.cloudpayments.sdk.ui.dialogs.base.BasePaymentBottomSheetFragment import ru.cloudpayments.sdk.ui.dialogs.PaymentCardFragment import ru.cloudpayments.sdk.ui.dialogs.PaymentOptionsFragment @@ -35,6 +36,8 @@ internal class PaymentActivity: FragmentActivity(), BasePaymentBottomSheetFragme PaymentOptionsFragment.IPaymentOptionsFragment, PaymentCardFragment.IPaymentCardFragment, PaymentProcessFragment.IPaymentProcessFragment { + val payParams: PayParams = PayParams() + companion object { private const val REQUEST_CODE_GOOGLE_PAY = 1 @@ -76,7 +79,7 @@ internal class PaymentActivity: FragmentActivity(), BasePaymentBottomSheetFragme val token = Base64.decode(result.paymentToken.toString(), Base64.DEFAULT) val runnable = { - val fragment = PaymentProcessFragment.newInstance(String(token)) + val fragment = PaymentProcessFragment.newInstance(PaymentProcessFragment.MODE_YANDEX_PAY, String(token)) nextFragment(fragment, true, R.id.frame_content) } Handler().postDelayed(runnable, 1000) @@ -167,13 +170,16 @@ internal class PaymentActivity: FragmentActivity(), BasePaymentBottomSheetFragme override fun onCardClicked() { val fragment = PaymentCardFragment.newInstance() fragment.show(supportFragmentManager, "") - //nextFragment(fragment, true, R.id.frame_content) + } + + override fun onTinkoffPayClicked() { + val fragment = PaymentProcessFragment.newInstance(PaymentProcessFragment.MODE_TINKOFF_PAY) + fragment.show(supportFragmentManager, "") } override fun onPayClicked(cryptogram: String) { - val fragment = PaymentProcessFragment.newInstance(cryptogram) + val fragment = PaymentProcessFragment.newInstance(PaymentProcessFragment.MODE_CARD, cryptogram) fragment.show(supportFragmentManager, "") - //nextFragment(fragment, true, R.id.frame_content) } override fun onPaymentFinished(transactionId: Int) { @@ -231,7 +237,7 @@ internal class PaymentActivity: FragmentActivity(), BasePaymentBottomSheetFragme if (token != null) { val runnable = { - val fragment = PaymentProcessFragment.newInstance(token) + val fragment = PaymentProcessFragment.newInstance(PaymentProcessFragment.MODE_GOOGLE_PAY, token) nextFragment(fragment, true, R.id.frame_content) } Handler().postDelayed(runnable, 1000) diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentOptionsFragment.kt b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentOptionsFragment.kt index 5700430..77f228b 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentOptionsFragment.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentOptionsFragment.kt @@ -2,10 +2,13 @@ package ru.cloudpayments.sdk.ui.dialogs import android.os.Bundle import android.text.Editable -import android.util.Log +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.PopupWindow +import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.fragment.app.viewModels import com.google.android.material.checkbox.MaterialCheckBox @@ -19,6 +22,7 @@ import com.yandex.pay.core.data.OrderDetails import com.yandex.pay.core.data.OrderID import com.yandex.pay.core.data.PaymentMethod import com.yandex.pay.core.data.PaymentMethodType +import ru.cloudpayments.sdk.R import ru.cloudpayments.sdk.databinding.DialogCpsdkPaymentOptionsBinding import ru.cloudpayments.sdk.ui.PaymentActivity import ru.cloudpayments.sdk.ui.dialogs.base.BasePaymentBottomSheetFragment @@ -29,12 +33,12 @@ import ru.cloudpayments.sdk.util.hideKeyboard import ru.cloudpayments.sdk.viewmodel.PaymentOptionsViewModel import ru.cloudpayments.sdk.viewmodel.PaymentOptionsViewState - internal class PaymentOptionsFragment : BasePaymentBottomSheetFragment() { interface IPaymentOptionsFragment { fun onGooglePayClicked() fun onCardClicked() + fun onTinkoffPayClicked() } companion object { @@ -51,7 +55,7 @@ internal class PaymentOptionsFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = DialogCpsdkPaymentOptionsBinding.inflate(inflater, container, false) return binding.root } @@ -82,6 +86,31 @@ internal class PaymentOptionsFragment : PublicKey.getInstance(it).saveVersion(state.publicKeyVersion) } } + + binding.buttonTinkoffPay.visibility = if (state.isTinkoffPayAvailable == true) View.VISIBLE else View.GONE + + checkSaveCardState(state) + } + + private fun checkSaveCardState (state: PaymentOptionsViewState) { + + paymentConfiguration?.paymentData?.accountId?.let { accountId -> + if (accountId.isNotEmpty()) { + if (paymentConfiguration?.paymentData?.jsonDataHasRecurrent() == true && state.isSaveCard == 1) { + + setSaveCardHintVisible() + } + if (paymentConfiguration?.paymentData?.jsonDataHasRecurrent() == true && state.isSaveCard == 2) { + setSaveCardHintVisible() + } + if (paymentConfiguration?.paymentData?.jsonDataHasRecurrent() == false && state.isSaveCard == 2) { + setSaveCardCheckBoxVisible() + } + if (state.isSaveCard == 3) { + setSaveCardHintVisible() + } + } + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -131,11 +160,7 @@ internal class PaymentOptionsFragment : binding.buttonPayCard.setOnClickListener { - if (paymentConfiguration!!.requireEmail || binding.checkboxSendReceipt.isChecked) { - paymentConfiguration?.paymentData?.email = binding.editEmail.text.toString() - } else { - paymentConfiguration?.paymentData?.email = "" - } + updateEmail() val listener = requireActivity() as? IPaymentOptionsFragment listener?.onCardClicked() @@ -175,7 +200,64 @@ internal class PaymentOptionsFragment : dismiss() } + binding.buttonTinkoffPay.setOnClickListener { + updateEmail() + updateSaveCard() + + val listener = requireActivity() as? IPaymentOptionsFragment + listener?.onTinkoffPayClicked() + dismiss() + } + + binding.buttonSaveCardPopup.setOnClickListener { + showPopupSaveCardInfo() + } + + binding.buttonCardBeSavedPopup.setOnClickListener { + showPopupSaveCardInfo() + } + viewModel.getPublicKey() + viewModel.getMerchantConfiguration(paymentConfiguration!!.publicId) + } + + private fun updateEmail() { + if (paymentConfiguration!!.requireEmail || binding.checkboxSendReceipt.isChecked) { + paymentConfiguration?.paymentData?.email = binding.editEmail.text.toString() + } else { + paymentConfiguration?.paymentData?.email = "" + } + } + + private fun updateSaveCard() { + if (binding.checkboxSaveCard.visibility == View.VISIBLE) { + (activity as PaymentActivity).payParams.saveCard = binding.checkboxSaveCard.isChecked + } + } + + private fun setSaveCardCheckBoxVisible() { + binding.checkboxSaveCard.visibility = View.VISIBLE + binding.buttonSaveCardPopup.visibility = View.VISIBLE + binding.checkboxSaveCard.checkedState = MaterialCheckBox.STATE_CHECKED + } + + private fun setSaveCardHintVisible() { + binding.textCardBeSaved.visibility = View.VISIBLE + binding.buttonCardBeSavedPopup.visibility = View.VISIBLE + } + + private fun showPopupSaveCardInfo() { + val popupView = layoutInflater.inflate(R.layout.popup_cpsdk_save_card_info, null) + + val wid = LinearLayout.LayoutParams.WRAP_CONTENT + val high = LinearLayout.LayoutParams.WRAP_CONTENT + val focus= true + val popupWindow = PopupWindow(popupView, wid, high, focus) + + val background = activity?.let { ContextCompat.getDrawable(it, R.drawable.cpsdk_bg_popup) } + popupView.background = background + + popupWindow.showAtLocation(view, Gravity.CENTER, 0, 0) } private fun updateStateButtons() { diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentProcessFragment.kt b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentProcessFragment.kt index fea3740..7d113a3 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentProcessFragment.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/PaymentProcessFragment.kt @@ -1,14 +1,19 @@ package ru.cloudpayments.sdk.ui.dialogs +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.view.isInvisible import androidx.fragment.app.viewModels import ru.cloudpayments.sdk.R import ru.cloudpayments.sdk.databinding.DialogCpsdkPaymentProcessBinding import ru.cloudpayments.sdk.models.ApiError +import ru.cloudpayments.sdk.ui.PaymentActivity import ru.cloudpayments.sdk.ui.dialogs.base.BasePaymentDialogFragment import ru.cloudpayments.sdk.util.InjectorUtils import ru.cloudpayments.sdk.viewmodel.PaymentProcessViewModel @@ -16,6 +21,7 @@ import ru.cloudpayments.sdk.viewmodel.PaymentProcessViewState internal enum class PaymentProcessStatus { InProcess, + TinkoffPay, Succeeded, Failed; } @@ -30,11 +36,22 @@ internal class PaymentProcessFragment: BasePaymentDialogFragment { binding.iconStatus.setImageResource(R.drawable.cpsdk_ic_progress) binding.textStatus.setText(R.string.cpsdk_text_process_title) + binding.textDescription.text = "" binding.buttonFinish.isInvisible = true } + PaymentProcessStatus.TinkoffPay -> { + binding.iconStatus.setImageResource(R.drawable.cpsdk_ic_progress) + binding.textStatus.setText(R.string.cpsdk_text_process_title_tinkoff_pay) + binding.textDescription.setText(R.string.cpsdk_text_process_description_tinkoff_pay) + binding.buttonFinish.isInvisible = false + binding.buttonFinish.setText(R.string.cpsdk_text_process_button_tinkoff_pay) + binding.buttonFinish.setBackgroundResource(R.drawable.cpsdk_bg_rounded_white_button_with_border) + binding.buttonFinish.setTextColor(context?.let { ContextCompat.getColor(it, R.color.cpsdk_blue) } ?: 0xFFFFFF) + + binding.buttonFinish.setOnClickListener { + + val listener = requireActivity() as? IPaymentProcessFragment + listener?.retryPayment() + dismiss() + } + } + PaymentProcessStatus.Succeeded, PaymentProcessStatus.Failed -> { binding.buttonFinish.isInvisible = false + binding.buttonFinish.setBackgroundResource(R.drawable.cpsdk_bg_rounded_blue_button) + binding.buttonFinish.setTextColor(context?.let { ContextCompat.getColor(it, R.color.cpsdk_white) } ?: 0xFFFFFF) val listener = requireActivity() as? IPaymentProcessFragment if (status == PaymentProcessStatus.Succeeded) { binding.iconStatus.setImageResource(R.drawable.cpsdk_ic_success) binding.textStatus.setText(R.string.cpsdk_text_process_title_success) + binding.textDescription.text = "" binding.buttonFinish.setText(R.string.cpsdk_text_process_button_success) listener?.onPaymentFinished(currentState?.transaction?.transactionId ?: 0) diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/ThreeDsDialogFragment.kt b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/ThreeDsDialogFragment.kt index 2d1363e..555e3e5 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/ThreeDsDialogFragment.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/ui/dialogs/ThreeDsDialogFragment.kt @@ -120,8 +120,8 @@ class ThreeDsDialogFragment : DialogFragment() { @JavascriptInterface fun processHTML(html: String?) { val doc: Document = Jsoup.parse(html) - val element: Element = doc.select("body").first() - val jsonObject = JsonParser().parse(element.ownText()).asJsonObject + val element: Element? = doc.select("body").first() + val jsonObject = JsonParser().parse(element?.ownText()).asJsonObject val paRes = jsonObject["PaRes"].asString requireActivity().runOnUiThread { if (!paRes.isNullOrEmpty()) { diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/util/Constants.kt b/sdk/src/main/java/ru/cloudpayments/sdk/util/Constants.kt index edbbd55..f8d503c 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/util/Constants.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/util/Constants.kt @@ -3,6 +3,8 @@ package ru.cloudpayments.sdk.util import com.google.android.gms.wallet.WalletConstants import ru.cloudpayments.sdk.BuildConfig +val TAG = "Payment SDK" + val GOOGLE_PAY_ENVIRONMENT = if (BuildConfig.DEBUG) WalletConstants.ENVIRONMENT_TEST else WalletConstants.ENVIRONMENT_PRODUCTION diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/util/InjectorUtils.kt b/sdk/src/main/java/ru/cloudpayments/sdk/util/InjectorUtils.kt index f9b5acc..12e1934 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/util/InjectorUtils.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/util/InjectorUtils.kt @@ -4,7 +4,7 @@ import ru.cloudpayments.sdk.configuration.PaymentData import ru.cloudpayments.sdk.viewmodel.PaymentProcessViewModelFactory internal object InjectorUtils { - fun providePaymentProcessViewModelFactory(paymentData: PaymentData, cryptogram: String, useDualMessagePayment: Boolean): PaymentProcessViewModelFactory { - return PaymentProcessViewModelFactory(paymentData, cryptogram, useDualMessagePayment) + fun providePaymentProcessViewModelFactory(paymentData: PaymentData, cryptogram: String, useDualMessagePayment: Boolean, saveCard: Boolean?): PaymentProcessViewModelFactory { + return PaymentProcessViewModelFactory(paymentData, cryptogram, useDualMessagePayment, saveCard) } } \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentOptionsViewModel.kt b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentOptionsViewModel.kt index 79b0f41..0d01440 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentOptionsViewModel.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentOptionsViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.MutableLiveData import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import ru.cloudpayments.sdk.api.CloudpaymentsApi -import ru.cloudpayments.sdk.util.PublicKey import javax.inject.Inject internal class PaymentOptionsViewModel: BaseViewModel() { @@ -25,11 +24,33 @@ internal class PaymentOptionsViewModel: BaseViewModel() .map { response -> val state = currentState.copy(publicKeyPem = response.pem, publicKeyVersion = response.version) stateChanged(state) - //checkTransactionResponse(response) - //Log.e("","") } .onErrorReturn { - // ERROR + + } + .subscribe() + } + + fun getMerchantConfiguration(publicId: String) { + disposable = api.getMerchantConfiguration(publicId) + .toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .map { response -> + + var isTinkoffPayAvailable = false + + for (paymentMethod in response.model?.externalPaymentMethods!!) { + if (paymentMethod.type == 6) { + isTinkoffPayAvailable = paymentMethod.enabled!! + break + } + } + + val state = currentState.copy(isTinkoffPayAvailable = isTinkoffPayAvailable, isSaveCard = response.model?.features?.isSaveCard) + stateChanged(state) + } + .onErrorReturn { + } .subscribe() } @@ -50,5 +71,7 @@ internal class PaymentOptionsViewModel: BaseViewModel() internal data class PaymentOptionsViewState( val publicKeyPem: String? = null, - val publicKeyVersion: Int? = null + val publicKeyVersion: Int? = null, + val isTinkoffPayAvailable: Boolean? = null, + val isSaveCard: Int? = null ): BaseViewState() \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModel.kt b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModel.kt index adff718..6823eec 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModel.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModel.kt @@ -1,20 +1,17 @@ package ru.cloudpayments.sdk.viewmodel -import android.util.Base64 -import android.util.Log import androidx.lifecycle.MutableLiveData import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import kotlinx.serialization.json.Json import ru.cloudpayments.sdk.api.CloudpaymentsApi -import ru.cloudpayments.sdk.api.models.CardCryptogramPacket -import ru.cloudpayments.sdk.api.models.CardInfo import ru.cloudpayments.sdk.api.models.CloudpaymentsTransaction import ru.cloudpayments.sdk.api.models.CloudpaymentsTransactionResponse import ru.cloudpayments.sdk.api.models.PaymentRequestBody +import ru.cloudpayments.sdk.api.models.QrLinkStatusWaitBody +import ru.cloudpayments.sdk.api.models.QrLinkStatusWaitResponse +import ru.cloudpayments.sdk.api.models.TinkoffPayQrLinkBody import ru.cloudpayments.sdk.configuration.PaymentData import ru.cloudpayments.sdk.ui.dialogs.PaymentProcessStatus import javax.inject.Inject @@ -22,7 +19,8 @@ import javax.inject.Inject internal class PaymentProcessViewModel( private val paymentData: PaymentData, private val cryptogram: String, - private val useDualMessagePayment: Boolean + private val useDualMessagePayment: Boolean, + private val saveCard: Boolean? ): BaseViewModel() { override var currentState = PaymentProcessViewState() override val viewState: MutableLiveData by lazy { @@ -103,11 +101,101 @@ internal class PaymentProcessViewModel( .subscribe() } + fun getTinkoffQrPayLink() { + + val jsonDataMap: HashMap = if (paymentData.jsonData != null && paymentData.jsonData.isNotEmpty()) { + Gson().fromJson(paymentData.jsonData, object : TypeToken?>() {}.type) + } else { + HashMap() + } + + val jsonDataString = if (jsonDataMap != null) { + Gson().toJson(jsonDataMap) + } else { + "" + } + + val body = TinkoffPayQrLinkBody(amount = paymentData.amount, + currency = paymentData.currency, + description = paymentData.description ?: "", + accountId = paymentData.accountId ?: "", + email = paymentData.email ?: "", + jsonData = jsonDataString, + invoiceId = paymentData.invoiceId ?: "", + scheme = if (useDualMessagePayment) "auth" else "charge") + + if (saveCard != null) { + body.saveCard = saveCard + } + + disposable = api.getTinkoffPayQrLink(body) + .toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .map { response -> + val state = if (response.success == true) { + currentState.copy(qrUrl = response.transaction?.qrUrl, transactionId = response.transaction?.transactionId) + } else { + currentState.copy(status = PaymentProcessStatus.Failed) + } + stateChanged(state) + } + .onErrorReturn { + val state = currentState.copy(status = PaymentProcessStatus.Failed) + stateChanged(state) + } + .subscribe() + } + + fun qrLinkStatusWait(transactionId: Int?) { + + val body = QrLinkStatusWaitBody(transactionId ?: 0) + + disposable = api.qrLinkStatusWait(body) + .toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .map { response -> + checkQrLinkStatusWaitResponse(response) + } + .onErrorReturn { + val state = currentState.copy(status = PaymentProcessStatus.Failed) + stateChanged(state) + } + .subscribe() + } + + private fun checkQrLinkStatusWaitResponse(response: QrLinkStatusWaitResponse) { + + if (response.success == true) { + when (response.transaction?.status) { + "Authorized", "Completed", "Cancelled" -> { + val state = currentState.copy(status = PaymentProcessStatus.Succeeded, transactionId = response.transaction.transactionId) + stateChanged(state) + } + "Declined" -> { + val state = currentState.copy(status = PaymentProcessStatus.Failed, transactionId = response.transaction.transactionId) + stateChanged(state) + } + else -> { + qrLinkStatusWait(response.transaction?.transactionId) + } + } + + } else { + val state = currentState.copy(status = PaymentProcessStatus.Failed, transactionId = response.transaction?.transactionId) + stateChanged(state) + } + } + fun clearThreeDsData(){ val state = currentState.copy(acsUrl = null, paReq = null) stateChanged(state) } + fun clearQrLinkData(){ + val state = currentState.copy(qrUrl = null) + stateChanged(state) + } + private fun checkTransactionResponse(transactionResponse: CloudpaymentsTransactionResponse){ val state = if (transactionResponse.success == true) { currentState.copy( @@ -167,5 +255,7 @@ internal data class PaymentProcessViewState( val paReq: String? = null, val acsUrl: String? = null, val errorMessage: String? = null, - val reasonCode: Int? = null + val reasonCode: Int? = null, + val qrUrl: String? = null, + val transactionId: Int? = null ): BaseViewState() \ No newline at end of file diff --git a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModelFactory.kt b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModelFactory.kt index aff661a..e75983b 100644 --- a/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModelFactory.kt +++ b/sdk/src/main/java/ru/cloudpayments/sdk/viewmodel/PaymentProcessViewModelFactory.kt @@ -7,11 +7,12 @@ import ru.cloudpayments.sdk.configuration.PaymentData internal class PaymentProcessViewModelFactory( private val paymentData: PaymentData, private val cryptogram: String, - private val useDualMessagePayment: Boolean + private val useDualMessagePayment: Boolean, + private val saveCard: Boolean? ): ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return PaymentProcessViewModel(paymentData, cryptogram, useDualMessagePayment) as T + return PaymentProcessViewModel(paymentData, cryptogram, useDualMessagePayment, saveCard) as T } } \ No newline at end of file diff --git a/sdk/src/main/res/drawable-hdpi/cpsdk_button_tinkoff_pay.png b/sdk/src/main/res/drawable-hdpi/cpsdk_button_tinkoff_pay.png new file mode 100644 index 0000000..43dc197 Binary files /dev/null and b/sdk/src/main/res/drawable-hdpi/cpsdk_button_tinkoff_pay.png differ diff --git a/sdk/src/main/res/drawable-mdpi/cpsdk_button_tinkoff_pay.png b/sdk/src/main/res/drawable-mdpi/cpsdk_button_tinkoff_pay.png new file mode 100644 index 0000000..d7c123b Binary files /dev/null and b/sdk/src/main/res/drawable-mdpi/cpsdk_button_tinkoff_pay.png differ diff --git a/sdk/src/main/res/drawable-xhdpi/cpsdk_button_tinkoff_pay.png b/sdk/src/main/res/drawable-xhdpi/cpsdk_button_tinkoff_pay.png new file mode 100644 index 0000000..d813b40 Binary files /dev/null and b/sdk/src/main/res/drawable-xhdpi/cpsdk_button_tinkoff_pay.png differ diff --git a/sdk/src/main/res/drawable-xxhdpi/cpsdk_button_tinkoff_pay.png b/sdk/src/main/res/drawable-xxhdpi/cpsdk_button_tinkoff_pay.png new file mode 100644 index 0000000..532b639 Binary files /dev/null and b/sdk/src/main/res/drawable-xxhdpi/cpsdk_button_tinkoff_pay.png differ diff --git a/sdk/src/main/res/drawable-xxxhdpi/cpsdk_button_tinkoff_pay.png b/sdk/src/main/res/drawable-xxxhdpi/cpsdk_button_tinkoff_pay.png new file mode 100644 index 0000000..a8172c2 Binary files /dev/null and b/sdk/src/main/res/drawable-xxxhdpi/cpsdk_button_tinkoff_pay.png differ diff --git a/sdk/src/main/res/drawable/cpsdk_bg_popup.xml b/sdk/src/main/res/drawable/cpsdk_bg_popup.xml new file mode 100644 index 0000000..73dc81a --- /dev/null +++ b/sdk/src/main/res/drawable/cpsdk_bg_popup.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/sdk/src/main/res/drawable/cpsdk_bg_rounded_black_button.xml b/sdk/src/main/res/drawable/cpsdk_bg_rounded_black_button.xml new file mode 100644 index 0000000..fc2d260 --- /dev/null +++ b/sdk/src/main/res/drawable/cpsdk_bg_rounded_black_button.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/sdk/src/main/res/drawable/cpsdk_bg_rounded_white_button_with_border.xml b/sdk/src/main/res/drawable/cpsdk_bg_rounded_white_button_with_border.xml new file mode 100644 index 0000000..f3df6fc --- /dev/null +++ b/sdk/src/main/res/drawable/cpsdk_bg_rounded_white_button_with_border.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/sdk/src/main/res/drawable/cpsdk_ic_blue_circle.xml b/sdk/src/main/res/drawable/cpsdk_ic_blue_circle.xml new file mode 100644 index 0000000..1f19cf5 --- /dev/null +++ b/sdk/src/main/res/drawable/cpsdk_ic_blue_circle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/sdk/src/main/res/drawable/cpsdk_ic_save_card_popup.xml b/sdk/src/main/res/drawable/cpsdk_ic_save_card_popup.xml new file mode 100644 index 0000000..f7e54d3 --- /dev/null +++ b/sdk/src/main/res/drawable/cpsdk_ic_save_card_popup.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/sdk/src/main/res/layout/dialog_cpsdk_payment_options.xml b/sdk/src/main/res/layout/dialog_cpsdk_payment_options.xml index 6ba8171..db3bee0 100644 --- a/sdk/src/main/res/layout/dialog_cpsdk_payment_options.xml +++ b/sdk/src/main/res/layout/dialog_cpsdk_payment_options.xml @@ -44,6 +44,17 @@ android:layout_height="wrap_content" android:text="@string/cpsdk_text_options_card" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/checkbox_save_card" /> @@ -108,9 +149,9 @@ style="@style/cpsdk_TextInputEditText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxLines="1" + android:imeOptions="actionDone" android:inputType="textEmailAddress" - android:imeOptions="actionDone"/> + android:maxLines="1" /> + + + + + app:layout_constraintTop_toBottomOf="@id/text_card_be_saved" /> diff --git a/sdk/src/main/res/layout/popup_cpsdk_save_card_info.xml b/sdk/src/main/res/layout/popup_cpsdk_save_card_info.xml new file mode 100644 index 0000000..76a2e19 --- /dev/null +++ b/sdk/src/main/res/layout/popup_cpsdk_save_card_info.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/src/main/res/values/colors.xml b/sdk/src/main/res/values/colors.xml index 2e4539e..ff7c409 100644 --- a/sdk/src/main/res/values/colors.xml +++ b/sdk/src/main/res/values/colors.xml @@ -9,6 +9,8 @@ #4297df #ec4444 #ffffff + #000000 + #2e71fc #64000000 #8C949F \ No newline at end of file diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index 0456d00..4dcf09d 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -3,10 +3,14 @@ CloudPaymentsSDK Выберите способ оплаты Банковская карта + Сохранить карту + Нажимая на чек-бокс, вы соглашаетесь с тем, что данные вашей карты сохранятся. Это означает, что при повторной оплате вам не надо будет вводить платежные реквизиты заново. + При этом мы не сможем проводить никакие операции по вашей карте без вашего согласия. Отправить квитанцию на E-mail E-mail Некорректный e-mail Для оплаты введите E-mail + При оплате данные вашей карты сохранятся Оплата картой Номер карты @@ -20,6 +24,11 @@ Операция отклонена Повторить попытку + Ждем ответа\noт Тинькофф Pay + Если перейти и оплатить в приложении не удалось, попробуйте снова или выберите другой способ оплаты + Выбрать способ оплаты + + %.2f\u20BD