diff --git a/.github/workflows/app_center.yml b/.github/workflows/app_center.yml index 3a7ce280..60276a97 100644 --- a/.github/workflows/app_center.yml +++ b/.github/workflows/app_center.yml @@ -2,6 +2,11 @@ name: publish demo to AppCenter on: workflow_dispatch: + inputs: + diff_with_origin: + description: 'diff with origin' + required: true + default: 'origin/master' jobs: build: @@ -16,6 +21,12 @@ jobs: java-version: '11' - name: build release run: ./gradlew :sample:assembleDebug + - name: get the realease Notes + run: | + NEW_CHANGES=$(./gradlew -q :realeaseNotes -PdiffWithOrigin='${{ github.event.inputs.diff_with_origin }}') + echo "NEW_CHANGES<> $GITHUB_ENV + echo "$NEW_CHANGES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@v1 with: @@ -23,5 +34,11 @@ jobs: token: ${{secrets.APP_CENTER_TOKEN}} group: Collaborators file: sample/build/outputs/apk/debug/sample-debug.apk - notifyTesters: false - debug: false \ No newline at end of file + notifyTesters: true + debug: false + releaseNotes: |+ + Branch name : ${{ github.head_ref || github.ref_name }} + + Last changes : + + ${{ env.NEW_CHANGES }} \ No newline at end of file diff --git a/.github/workflows/merge_request.yml b/.github/workflows/merge_request.yml index 99fa5b13..b1ad6cf5 100644 --- a/.github/workflows/merge_request.yml +++ b/.github/workflows/merge_request.yml @@ -8,4 +8,4 @@ on: jobs: check: - uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.merge_request.yml@v1 + uses: tinkoff-mobile-tech/workflows/.github/workflows/android_lib.merge_request.yml@v1 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9d1116e1..103fb075 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,10 @@ -apply plugin: 'kotlin' -apply plugin: 'org.jetbrains.dokka' -apply from: 'gradle/versions.gradle' - buildscript { apply from: 'gradle/versions.gradle' repositories { google() + jcenter() + mavenCentral() maven { url 'https://plugins.gradle.org/m2/' } } @@ -17,6 +15,14 @@ buildscript { } } +plugins { + id 'org.ajoberstar.grgit' version '5.0.0' +} + +apply plugin: 'kotlin' +apply plugin: 'org.jetbrains.dokka' +apply from: 'gradle/versions.gradle' + allprojects { repositories { google() @@ -28,6 +34,17 @@ allprojects { } } +tasks.register("realeaseNotes") { + doLast { + def branch = grgit.branch.current().fullName + def log = grgit + .log { range branch, diffWithOrigin } + .findAll { !it.shortMessage.startsWith("Merge") } + .collect { it.shortMessage } + .join("\n\n") + print log + } +} dokkaHtmlMultiModule.configure { outputDirectory.set(new File("$buildDir/dokka")) } \ No newline at end of file diff --git a/cardio/src/main/java/ru/tinkoff/cardio/CameraCardIOScannerContract.kt b/cardio/src/main/java/ru/tinkoff/cardio/CameraCardIOScannerContract.kt new file mode 100644 index 00000000..91a5b44b --- /dev/null +++ b/cardio/src/main/java/ru/tinkoff/cardio/CameraCardIOScannerContract.kt @@ -0,0 +1,73 @@ +/* + * 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.cardio + +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import io.card.payment.CardIOActivity +import io.card.payment.CreditCard +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.CardScannerContract +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.ScannedCardResult +import ru.tinkoff.acquiring.sdk.cardscanners.models.AsdkScannedCardData +import ru.tinkoff.acquiring.sdk.cardscanners.models.ScannedCardData +import java.util.* + +/** + * @author Mariya Chernyadieva + */ +object CameraCardIOScannerContract : CardScannerContract() { + + override fun createIntent(context: Context, input: Unit): Intent { + return createIntent(context) + } + + override fun parseResult(resultCode: Int, intent: Intent?): ScannedCardResult { + return when (resultCode) { + RESULT_OK -> ScannedCardResult.Success(parseIntentData(intent!!)) + RESULT_CANCELED -> ScannedCardResult.Cancel + else -> ScannedCardResult.Failure(null) + } + } + + private fun parseIntentData(data: Intent): ScannedCardData { + val cardNumber: String + var expireDate = "" + val cardholderName = "" + + val scanResult = data.getParcelableExtra(CardIOActivity.EXTRA_SCAN_RESULT) + cardNumber = scanResult!!.formattedCardNumber + if (scanResult.expiryMonth != 0 && scanResult.expiryYear != 0) { + val locale = Locale.getDefault() + val expiryYear = scanResult.expiryYear % 100 + expireDate = String.format(locale, "%02d%02d", scanResult.expiryMonth, expiryYear) + } + + return AsdkScannedCardData(cardNumber, expireDate, cardholderName) + } + + private fun createIntent(context: Context): Intent { + val scanIntent = Intent(context, CardIOActivity::class.java) + return scanIntent.apply { + putExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, true) + putExtra(CardIOActivity.EXTRA_REQUIRE_CVV, false) + putExtra(CardIOActivity.EXTRA_REQUIRE_POSTAL_CODE, false) + putExtra(CardIOActivity.EXTRA_SUPPRESS_CONFIRMATION, true) + } + } +} \ No newline at end of file diff --git a/changelog.md b/changelog.md index 5c66e181..715bc7f6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,15 @@ +## 3.0.0 + +#### Fixed +#### Changes +- add MirPay payment method +- redesing sdk screens +- !up targetSdk version to 33! +- new payment process entities ([migration](/migration.md)) +- new methods for screen launching ([migration](/migration.md)) +- new card scan methods `CardScannerNewApi.kt` +#### Additions + ## 2.13.2 #### Fixed 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 732b7e00..a5a772be 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/AcquiringSdk.kt @@ -20,6 +20,7 @@ import ru.tinkoff.acquiring.sdk.loggers.JavaLogger import ru.tinkoff.acquiring.sdk.loggers.Logger import ru.tinkoff.acquiring.sdk.requests.* import ru.tinkoff.acquiring.sdk.responses.TinkoffPayStatusResponse +import ru.tinkoff.acquiring.sdk.utils.EnvironmentMode import ru.tinkoff.acquiring.sdk.utils.SampleAcquiringTokenGenerator import ru.tinkoff.acquiring.sdk.utils.keycreators.KeyCreator import ru.tinkoff.acquiring.sdk.utils.keycreators.StringKeyCreator @@ -222,6 +223,17 @@ class AcquiringSdk( fun tinkoffPayLink(paymentId: Long, version: String, request: (TinkoffPayLinkRequest.() -> Unit)? = null): TinkoffPayLinkRequest { return TinkoffPayLinkRequest(paymentId.toString(), version).apply { + terminalKey = this@AcquiringSdk.terminalKey + request?.invoke(this) + } + } + + /** + * Метод получения Deeplink-a для оплаты с помощью MirPay + */ + fun mirPayLink(paymentId: Long, request: (MirPayLinkRequest.() -> Unit)? = null): MirPayLinkRequest { + return MirPayLinkRequest(paymentId.toString()).apply { + terminalKey = this@AcquiringSdk.terminalKey request?.invoke(this) } } @@ -260,6 +272,11 @@ class AcquiringSdk( companion object { + /** + * Позволяет установить мод для окружения по умолчанию (дебаг) + */ + var environmentMode: EnvironmentMode = EnvironmentMode.IsDebugMode + /** * Объект, который будет использоваться для генерации токена при формировании запросов к api * ([документация по формированию токена](https://www.tinkoff.ru/kassa/develop/api/request-sign/)). @@ -284,12 +301,17 @@ class AcquiringSdk( */ var isDeveloperMode = false + /** + * Позволяет переключать SDK с тестового режима(на другой контур) и обратно. В тестовом режиме деньги с карты не + * списываются. По-умолчанию выключен + */ + var isPreprodMode = false + /** * Позволяет переключать SDK на иной апи-контур, работает только в дебаг режиме */ var customUrl : String? = null - /** * Логирует сообщение */ @@ -347,4 +369,4 @@ fun interface AcquiringTokenGenerator { .digest(source.toByteArray()) .joinToString("") { "%02x".format(it) } } -} \ No newline at end of file +} diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringApiException.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringApiException.kt index 593eac5f..34fa9f67 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringApiException.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringApiException.kt @@ -16,6 +16,7 @@ package ru.tinkoff.acquiring.sdk.exceptions +import ru.tinkoff.acquiring.sdk.network.AcquiringApi import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse /** @@ -40,4 +41,15 @@ class AcquiringApiException : Exception { constructor(response: AcquiringResponse) : super("") { this.response = response } +} + +fun Throwable.asAcquiringApiException() = (this as? AcquiringApiException) + +fun Exception.checkCustomerNotFoundError(): Boolean { + return getErrorCodeIfApiError() == AcquiringApi.API_ERROR_CODE_CUSTOMER_NOT_FOUND +} + +fun Exception.getErrorCodeIfApiError() : String? { + val api = (this as? AcquiringApiException) ?: return null + return api.response?.errorCode } \ No newline at end of file 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 3b83b45a..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, val paymentId: Long? = null) : 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 index c07ea939..62853daa 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/exceptions/AcquiringSdkTimeoutException.kt @@ -25,6 +25,6 @@ import ru.tinkoff.acquiring.sdk.models.enums.ResponseStatus */ class AcquiringSdkTimeoutException( val throwable: Throwable, - val paymentId: Long?, - val status: ResponseStatus?, + val paymentId: Long? = null, + val status: ResponseStatus? = null, ) : 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 eee8727e..1e4e6879 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 @@ -75,4 +75,4 @@ enum class ResponseStatus { } } } -} +} \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/SbpPay.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/SbpPay.kt new file mode 100644 index 00000000..fbfac499 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/models/paysources/SbpPay.kt @@ -0,0 +1,11 @@ +package ru.tinkoff.acquiring.sdk.models.paysources + +import ru.tinkoff.acquiring.sdk.models.PaymentSource + +/** + * Тип оплаты с помощью SBP Pay + * + * + * Created by i.golovachev + */ +object SbpPay : PaymentSource \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/network/AcquiringApi.kt index f885c28f..e384f939 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 @@ -44,6 +44,7 @@ object AcquiringApi { const val SUBMIT_3DS_AUTHORIZATION_V2 = "Submit3DSAuthorizationV2" const val COMPLETE_3DS_METHOD_V2 = "Complete3DSMethodv2" const val GET_TERMINAL_PAY_METHODS = "GetTerminalPayMethods" + const val MIR_PAY_GET_DEEPLINK_METHOD = "MirPay/GetDeepLink" const val API_ERROR_CODE_3DSV2_NOT_SUPPORTED = "106" const val API_ERROR_CODE_CUSTOMER_NOT_FOUND = "7" @@ -57,40 +58,9 @@ 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") /** * Коды ошибок, вызванные временными неполадками системы @@ -106,7 +76,7 @@ object AcquiringApi { internal const val API_REQUEST_METHOD_POST = "POST" internal const val API_REQUEST_METHOD_GET = "GET" - internal const val JSON = "application/json" + const val JSON = "application/json" internal const val FORM_URL_ENCODED = "application/x-www-form-urlencoded" internal const val TIMEOUT = 40000L @@ -117,6 +87,9 @@ object AcquiringApi { private const val API_URL_RELEASE = "https://securepay.tinkoff.ru/$API_VERSION" private const val API_URL_DEBUG = "https://rest-api-test.tinkoff.ru/$API_VERSION" + private const val API_URL_PREPROD_OLD = "https://qa-mapi.tcsbank.ru/rest" + private const val API_URL_PREPROD = "https://qa-mapi.tcsbank.ru/$API_VERSION" + private val oldMethodsList = listOf("Submit3DSAuthorization") /** @@ -141,6 +114,9 @@ object AcquiringApi { return oldMethodsList.contains(apiMethod) } - private fun useCustomOrDefault(default: String, custom: String?, oldOrV2: String = "v2") = - custom?.let { "$it/$oldOrV2" } ?: default + private fun useCustomOrDefault(default: String, custom: String?, oldOrV2: String = API_VERSION) = + custom?.let { + if (it.contains(oldOrV2)) it + else "$it/$oldOrV2" + } ?: default } \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt index 902aeefc..b6c9a8dd 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/AcquiringRequest.kt @@ -17,6 +17,9 @@ package ru.tinkoff.acquiring.sdk.requests import com.google.gson.Gson +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import okhttp3.Response @@ -27,6 +30,7 @@ import ru.tinkoff.acquiring.sdk.network.AcquiringApi.JSON import ru.tinkoff.acquiring.sdk.network.NetworkClient import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse import ru.tinkoff.acquiring.sdk.utils.Request +import ru.tinkoff.acquiring.sdk.utils.RequestResult import java.io.UnsupportedEncodingException import java.net.URLEncoder import java.security.PublicKey @@ -106,6 +110,18 @@ abstract class AcquiringRequest(internal val apiMethod: S } } + protected fun performRequestFlow(request: AcquiringRequest, + responseClass: Class) : Flow> { + request.validate() + val client = NetworkClient() + val flow = MutableStateFlow>(RequestResult.NotYet) + client.call(request, responseClass, + onSuccess = { flow.tryEmit(RequestResult.Success(it)) }, + onFailure = { flow.tryEmit(RequestResult.Failure(it)) } + ) + return flow + } + @kotlin.jvm.Throws(NetworkException::class) protected fun performRequestRaw(request: AcquiringRequest): Response { request.validate() diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetCardListRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetCardListRequest.kt index ced90bbe..f2940183 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetCardListRequest.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/GetCardListRequest.kt @@ -16,8 +16,10 @@ package ru.tinkoff.acquiring.sdk.requests +import kotlinx.coroutines.flow.Flow import ru.tinkoff.acquiring.sdk.network.AcquiringApi.GET_CARD_LIST_METHOD import ru.tinkoff.acquiring.sdk.responses.GetCardListResponse +import ru.tinkoff.acquiring.sdk.utils.RequestResult /** * Возвращает список привязанных карт у покупателя @@ -49,4 +51,11 @@ class GetCardListRequest : AcquiringRequest(GET_CARD_LIST_M override fun execute(onSuccess: (GetCardListResponse) -> Unit, onFailure: (Exception) -> Unit) { super.performRequest(this, GetCardListResponse::class.java, onSuccess, onFailure) } + + /** + * Реактивный вызов метода API + */ + fun executeFlow(): Flow> { + return super.performRequestFlow(this, GetCardListResponse::class.java) + } } \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/MirPayLinkRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/MirPayLinkRequest.kt new file mode 100644 index 00000000..112a352c --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/MirPayLinkRequest.kt @@ -0,0 +1,30 @@ +package ru.tinkoff.acquiring.sdk.requests + +import ru.tinkoff.acquiring.sdk.network.AcquiringApi +import ru.tinkoff.acquiring.sdk.network.AcquiringApi.MIR_PAY_GET_DEEPLINK_METHOD +import ru.tinkoff.acquiring.sdk.responses.MirPayResponse + +/** + * @author k.shpakovskiy + */ +class MirPayLinkRequest( + var paymentId: String +) : AcquiringRequest( + apiMethod = MIR_PAY_GET_DEEPLINK_METHOD +) { + + override val httpRequestMethod: String = AcquiringApi.API_REQUEST_METHOD_POST + + override fun asMap(): MutableMap { + val map = super.asMap() + map[PAYMENT_ID] = paymentId + return map + } + + override fun validate() { + paymentId.validate(PAYMENT_ID) + } + override fun execute(onSuccess: (MirPayResponse) -> Unit, onFailure: (Exception) -> Unit) { + super.performRequest(this, MirPayResponse::class.java, onSuccess, onFailure) + } +} diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/RemoveCardRequest.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/RemoveCardRequest.kt index 652f7d95..334318b1 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/RemoveCardRequest.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/requests/RemoveCardRequest.kt @@ -16,8 +16,10 @@ package ru.tinkoff.acquiring.sdk.requests +import kotlinx.coroutines.flow.Flow import ru.tinkoff.acquiring.sdk.network.AcquiringApi.REMOVE_CARD_METHOD import ru.tinkoff.acquiring.sdk.responses.RemoveCardResponse +import ru.tinkoff.acquiring.sdk.utils.RequestResult /** * Удаляет привязанную карту @@ -56,4 +58,11 @@ class RemoveCardRequest : AcquiringRequest(REMOVE_CARD_METHO override fun execute(onSuccess: (RemoveCardResponse) -> Unit, onFailure: (Exception) -> Unit) { super.performRequest(this, RemoveCardResponse::class.java, onSuccess, onFailure) } + + /** + * Реактивный вызов метода API + */ + fun executeFlow(): Flow> { + return super.performRequestFlow(this, RemoveCardResponse::class.java) + } } \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt index 34da66fb..f0271cd9 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/GetTerminalPayMethodsResponse.kt @@ -57,6 +57,9 @@ class PaymethodData( ) enum class Paymethod { + @SerializedName("MirPay") + MirPay, + @SerializedName("TinkoffPay") TinkoffPay, @@ -64,5 +67,10 @@ enum class Paymethod { YandexPay, @SerializedName("SBP") - SBP -} \ No newline at end of file + SBP, + + @SerializedName("Cards") + Cards, + + Unknown +} diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/MirPayResponse.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/MirPayResponse.kt new file mode 100644 index 00000000..e52bbb0e --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/responses/MirPayResponse.kt @@ -0,0 +1,15 @@ +package ru.tinkoff.acquiring.sdk.responses + +import com.google.gson.annotations.SerializedName + +/** + * Ответ на запрос /api/v2/MirPay/GetDeepLink + * @param deeplink Диплинк для перехода в приложение MirPay + * для совершения оплаты + * + * @author k.shpakovskiy + */ +class MirPayResponse( + @SerializedName("Deeplink") + val deeplink: String? = null +) : AcquiringResponse() diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/EnvironmentMode.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/EnvironmentMode.kt new file mode 100644 index 00000000..93ea3dbb --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/EnvironmentMode.kt @@ -0,0 +1,9 @@ +package ru.tinkoff.acquiring.sdk.utils + +sealed class EnvironmentMode { + + object IsPreProdMode : EnvironmentMode() + object IsDebugMode: EnvironmentMode() + object IsCustomMode: EnvironmentMode() + +} \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/Request.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/Request.kt index 1bd8b5c7..0241d0c2 100644 --- a/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/Request.kt +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/Request.kt @@ -16,10 +16,18 @@ package ru.tinkoff.acquiring.sdk.utils +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + /** * @author Mariya Chernyadieva */ interface Request : Disposable { fun execute(onSuccess: (R) -> Unit, onFailure: (Exception) -> Unit) + + suspend fun execute(): R = suspendCoroutine { cttn -> + execute(onSuccess = { cttn.resume(it) }, onFailure = { cttn.resumeWithException(it) }) + } } \ No newline at end of file diff --git a/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/RequestResult.kt b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/RequestResult.kt new file mode 100644 index 00000000..091747a0 --- /dev/null +++ b/core/src/main/java/ru/tinkoff/acquiring/sdk/utils/RequestResult.kt @@ -0,0 +1,21 @@ +package ru.tinkoff.acquiring.sdk.utils + +import ru.tinkoff.acquiring.sdk.responses.AcquiringResponse + +sealed class RequestResult { + + object NotYet: RequestResult() + + class Success(val result: R) : RequestResult() + + class Failure(val exception: java.lang.Exception) : RequestResult() + + fun process(onSuccess: (R) -> Unit, onFailure: (java.lang.Exception) -> Unit) { + when (this) { + is Success -> onSuccess(result) + is Failure -> onFailure(exception) + } + } + + val isFinished get() = this !is NotYet +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e99e14a5..3d3fb47a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=2.13.3 -VERSION_CODE=22 +VERSION_NAME=3.0.0 +VERSION_CODE=26 GROUP=ru.tinkoff.acquiring POM_DESCRIPTION=Library which allows you to use internet acquiring in your android app diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 57a65cda..c21847d2 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -2,9 +2,9 @@ ext { isRelease = project.hasProperty('release') versionName = isRelease ? VERSION_NAME : "$VERSION_NAME-SNAPSHOT" - compileSdk = 31 + compileSdk = 33 minSdk = 24 - targetSdk = 31 + targetSdk = 33 buildTools = '30.0.3' kotlinVersion = '1.6.10' @@ -12,12 +12,14 @@ ext { dokkaVersion = '1.7.10' appCompatVersion = '1.4.0' + fragmentKtxVersion = '1.5.5' preferenceVersion = '1.1.1' lifecycleExtensionsVersion = '2.5.1' cardIoVersion = '5.5.1' gsonVersion = '2.8.6' coreNfcVersion = '1.0.3' - coroutinesVersion = '1.3.7' + decoroVersion = '1.5.1' + coroutinesVersion = '1.6.4' googleWalletVersion = '18.0.0' constraintLayoutVersion = '1.1.3' @@ -29,9 +31,13 @@ ext { blurryVersion = '4.0.0' bouncyCastleVersion = '1.65' rootBeerVersion = '0.1.0' + lifecycleRuntimeVersion = '2.4.0' + recyclerviewVersion = '1.2.1' materialVersion = '1.5.0' - mokitoKotlin = '4.0.0' - yandexPayVersion = '0.2.1' + + mokitoKotlinVersion = '4.0.0' + mokitoInlineVersion = '3.5.13' + turbineVersion = '0.12.0' } diff --git a/images/attach.jpeg b/images/attach.jpeg deleted file mode 100644 index 10ffd81d..00000000 Binary files a/images/attach.jpeg and /dev/null differ diff --git a/images/card_pay.png b/images/card_pay.png new file mode 100644 index 00000000..c650d09b Binary files /dev/null and b/images/card_pay.png differ diff --git a/images/pay.jpeg b/images/pay.jpeg deleted file mode 100644 index 1acba1cf..00000000 Binary files a/images/pay.jpeg and /dev/null differ diff --git a/images/tpay.png b/images/tpay.png new file mode 100644 index 00000000..fc4a29ef Binary files /dev/null and b/images/tpay.png differ diff --git a/migration.md b/migration.md index e41aa6da..326c6885 100644 --- a/migration.md +++ b/migration.md @@ -1,3 +1,34 @@ +3.0.0 +Редизайн asdk +Некоторые методы `TinkoffAcquiring` для открытия экранов оплаты удалены, +новая версия sdk подразумевает использовать классы : +`MainFormLauncher.Contract`, +`TpayLauncher.Contract`, +`MirPayLauncher.Contract`, +`SbpPayLauncher.Contract`, +`PaymentByCardLauncher.Contract`, +`RecurrentPayLauncher.Contract`, +`AttachCardLauncher.Contract`, +`SavedCardsLauncher.Contract`, +`ChoseCardLauncher.Contract` +для открытия экранов используя new Result api. + +Методы `TinkoffAcquiring` для управления платежной сессией теперь Deprecated, +новая версия sdk подразумевает использовать классы : +`MirPayProcess`, +`PaymentByCardProcess`, +`RecurrentPaymentProcess`, +`SbpPaymentProcess`, +`TpayProcess`, +`YandexPaymentProcess`, +для использования бизнес-логики эквайринга. + +По дефолту поддерживается русская и английская локализация с помощью ресурсов, `AsdkLocalization` +больше не поддерживается. + +Метод `TinkoffAcquiring#checkTinkoffPayStatus` удален, в место него используйте `TinkoffAcquiring#checkTerminalInfo` +Метод `CameraCardScanner#startActivityForScanning` теперь Deprecated, используйте `CardScannerNewApi.kt` + 2.13.2 Новый алгоритм работы со сценарием оплаты СБП Новый инстанс ошибки `NspkOpenException` - выбрасывается при неуспешном открытии приложения - партнера @@ -54,4 +85,4 @@ Android-приложениях, также следует добавить до ```groovy implementation 'ru.tinkoff.acquiring:threeds-sdk:$latestVersion' implementation 'ru.tinkoff.acquiring:threeds-wrapper:$latestVersion' -``` \ No newline at end of file +``` diff --git a/readme.md b/readme.md index 63c5057d..3dd44525 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/ru.tinkoff.acquiring/ui.svg?maxAge=3600)][search.maven] - + Acquiring SDK позволяет интегрировать [Интернет-Эквайринг Tinkoff][acquiring] в мобильные приложения для платформы Android. @@ -17,8 +17,8 @@ Acquiring SDK позволяет интегрировать [Интернет-Э - Интеграция с онлайн-кассами; - Поддержка Системы быстрых платежей - Оплата через Tinkoff Pay +- Оплата через Mir Pay - Оплата с помощью Yandex Pay -- Совершение оплаты из уведомления ### Требования Для работы Tinkoff Acquiring SDK необходим Android версии 7.0 и выше (API level 24). @@ -75,41 +75,62 @@ AcquiringSdk.tokenGenerator = SampleAcquiringTokenGenerator(password) // ген ``` ### Пример работы -Для проведения оплаты необходимо вызвать метод **TinkoffAcquiring**#_openPaymentScreen_. Метод запустит экран оплаты **PaymentActivity**. Активити должна быть настроена на обработку конкретного платежа, поэтому в метод необходимо передать настройки проведения оплаты, включающие в себя данные заказа, данные покупателя и опционально параметры кастомизации экрана оплаты. -Кроме того, можно указать тему, указать локализацию формы (или передать свою), а также указать модуль для сканирования (свой или **CameraCardIOScanner**). - +Для начала необходимо создать **TinkoffAcquiring** ```kotlin -val paymentOptions = - PaymentOptions().setOptions { - orderOptions { // данные заказа - orderId = "ORDER-ID" // ID заказа в вашей системе - amount = Money.ofRubles(1000) // сумма для оплаты - title = "НАЗВАНИЕ ПЛАТЕЖА" // название платежа, видимое пользователю - description = "ОПИСАНИЕ ПЛАТЕЖА" // описание платежа, видимое пользователю - recurrentPayment = false // флаг определяющий является ли платеж рекуррентным [1] - successURL = "URL" // URL, куда будет переведен покупатель в случае успешной оплаты (см. полную документацию) - failURL = "URL" // URL, куда будет переведен покупатель в случае неуспешной оплаты (см. полную документацию) - } - customerOptions { // данные покупателя - checkType = CheckType.NO.toString() // тип привязки карты - customerKey = "CUSTOMER_KEY" // уникальный ID пользователя для сохранения данных его карты - email = "batman@gotham.co" // E-mail клиента для отправки уведомления об оплате - } - featuresOptions { // настройки визуального отображения и функций экрана оплаты - useSecureKeyboard = true // флаг использования безопасной клавиатуры [2] - cameraCardScanner = CameraCardIOScanner() - theme = themeId - } - } - val tinkoffAcquiring = TinkoffAcquiring(applicationContext, "TERMINAL_KEY", "PUBLIC_KEY") // создание объекта для взаимодействия с SDK и передача данных продавца -tinkoffAcquiring.openPaymentScreen(this@MainActivity, paymentOptions, PAYMENT_REQUEST_CODE) +``` +Далее необходимо проинициализировать платежные сессии методов оплаты: +```kotlin +tinkoffAcquiring.initSbpPaymentSession() +tinkoffAcquiring.initTinkoffPayPaymentSession() +tinkoffAcquiring.initMirPayPaymentSession() ``` -Результат вызова метода вернется в **onActivityResult**: -- при успешном платеже (_Activity.RESULT_OK_) возвращается _TinkoffAcquiring.EXTRA_PAYMENT_ID_ - идентификатор платежа типа Long, опционально _TinkoffAcquiring.EXTRA_CARD_ID_ - id карты, с которой проводился платеж, тип String и опционально _TinkoffAcquiring.EXTRA_REBILL_ID_ - rebillId карты, если был совершен рекуррентный платеж, тип String -- при неуспешном платеже (_TinkoffAcquiring.RESULT_ERROR_) возвращается ошибка _TinkoffAcquiring.EXTRA_ERROR_ типа Throwable (подробнее о возвращаемых ошибках в [документации][full-doc]) +> **Примечание** +> Для того чтобы не инициализировать все платежные сессии, можно предварительно +> вызвать метод **TinkoffAcquiring.checkTerminalInfo()** для получения списка +> актуальных способов оплаты и произвести инициализации платежных сессий только для них. + +Далее необходимо настроить экран с формой оплаты на обработку конкретного +платежа с помощью **PaymentOptions** которые состоят из настроек проведения +оплаты, в том числе данные заказа, данные покупателя и опционально параметры +кастомизации экрана оплаты. Так же можно указать модуль для сканирования +(свой или **CardScannerDelegate**). Локализация берется из системы, +так же имеется поддержка светлой и тёмной темы. Внешний вид экрана и набор +компонентов определяется из доступных методов оплаты, настраивается в личном кабинете. +```kotlin +val paymentOptions = + PaymentOptions().setOptions { + orderOptions { // данные заказа + orderId = "ORDER-ID" // ID заказа в вашей системе + amount = Money.ofRubles(1000) // сумма для оплаты + title = "НАЗВАНИЕ ПЛАТЕЖА" // название платежа, видимое пользователю + description = "ОПИСАНИЕ ПЛАТЕЖА" // описание платежа, видимое пользователю + recurrentPayment = false // флаг определяющий является ли платеж рекуррентным [1] + successURL = "URL" // URL, куда будет переведен покупатель в случае успешной оплаты (см. полную документацию) + failURL = "URL" // URL, куда будет переведен покупатель в случае неуспешной оплаты (см. полную документацию) + } + customerOptions { // данные покупателя + checkType = CheckType.NO.toString() // тип привязки карты + customerKey = "CUSTOMER_KEY" // уникальный ID пользователя для сохранения данных его карты + email = "batman@gotham.co" // E-mail клиента для отправки уведомления об оплате + } + featuresOptions { // настройки визуального отображения и функций экрана оплаты + cameraCardScanner = + CardScannerDelegateImpl() // реализация механизма сканирования карт, можно использовать встроенный CardScannerWrapper + } + } +``` +Затем регистрируем контракт **MainFormContract**#_Contract_, и вызываем метод **ActivityResultLauncher**#_launch_ +```kotlin +val byMainFormPayment = registerForActivityResult(MainFormLauncher.Contract, ActivityResultCallback {}) +byMainFormPayment.launch(MainFormContract.StartData(paymentOptions)) +``` +Результат платежа вернется в **ActivityResultCallback**: +- при успешном платеже возвращается _MainFormLauncher.Success_ - содержащий _paymentId_ идентификатор платежа, опционально _cardId_ - id карты, с которой проводился платеж, тип String и опционально _rebillId_ - rebillId карты, если был совершен рекуррентный платеж +- при неуспешном платеже (_MainFormLauncher.Error_) содержащий Throwable (подробнее о возвращаемых ошибках в [документации][full-doc]) +- при отмене платежа (_MainFormLauncher.Canceled_) -Можно передать данные чека, указав параметр **receipt** в методе **PaymentOptions**#_orderOptions_ и передать дополнительные параметры **additionalData**. Эти объекты при их наличии будут переданы на сервер с помощью метода [**API Init**][init-documentation], где можно посмотреть их детальное описание. +Можно так же передать данные чека, указав параметр **receipt** в методе **PaymentOptions**#_orderOptions_ и передать дополнительные параметры **additionalData**. Эти объекты при их наличии будут переданы на сервер с помощью метода [**API Init**][init-documentation], где можно посмотреть их детальное описание. ```kotlin val paymentOptions = @@ -127,8 +148,8 @@ val paymentOptions = } } -val tinkoffAcquiring = TinkoffAcquiring(applicationContext, "TERMINAL_KEY", "PUBLIC_KEY") -tinkoffAcquiring.openPaymentScreen(this@MainActivity, paymentOptions, PAYMENT_REQUEST_CODE) +val byMainFormPayment = registerForActivityResult(MainFormContract.Contract) +byMainFormPayment.launch(MainFormContract.StartData(paymentOptions)) ``` [1] _Рекуррентный платеж_ может производиться для дальнейшего списания средств с сохраненной карты, без ввода ее реквизитов. Эта возможность, например, может использоваться для осуществления платежей по подписке. @@ -136,10 +157,11 @@ tinkoffAcquiring.openPaymentScreen(this@MainActivity, paymentOptions, PAYMENT_RE ### Экран привязки карт -Для запуска экрана привязки карт необходимо запустить **TinkoffAcquiring**#_openAttachCardScreen_. В метод также необходимо передать некоторые параметры - тип привязки, данные покупателя и опционально параметры кастомизации (по-аналогии с экраном оплаты): +Для запуска экрана привязки карт необходимо зарегестирировать **AttachCardLauncher**#_Contract_. В метод также необходимо передать некоторые параметры - тип привязки, данные покупателя и опционально параметры кастомизации (по-аналогии с экраном оплаты): ```kotlin val attachCardOptions = AttachCardOptions().setOptions { + setTerminalParams("TERMINAL_KEY", "PUBLIC_KEY") customerOptions { // данные покупателя customerKey = "CUSTOMER_KEY" // уникальный ID пользователя для сохранения данных его карты checkType = CheckType.NO.toString() // тип привязки карты @@ -152,68 +174,71 @@ val attachCardOptions = } } -val tinkoffAcquiring = TinkoffAcquiring(applicationContext, "TERMINAL_KEY", "PUBLIC_KEY") -tinkoffAcquiring.openAttachCardScreen(this@MainActivity, attachCardOptions, ATTACH_CARD_REQUEST_CODE) +attachCard = registerForActivityResult(AttachCard.Contract, ActivityResultCallback {}) +attachCard.launch(options) ``` -Результат вызова метода вернется в **onActivityResult**: -- при успешной привязке (_Activity.RESULT_OK_) возвращается _TinkoffAcquiring.EXTRA_CARD_ID_ - id карты, которая была привязана, тип String -- при неуспешной привязке (_TinkoffAcquiring.RESULT_ERROR_) возвращается ошибка _TinkoffAcquiring.EXTRA_ERROR_ типа Throwable (подробнее о возвращаемых ошибках в [документации][full-doc]) +Результат вызова метода вернется в **ActivityResultCallback** в виде **AttachCard.Result**: +- при успешной привязке (_AttachCard.Success_) возвращается _cardId_ - id карты, которая была привязана, тип String +- при неуспешной привязке (_AttachCard.Error_) возвращается ошибка _error_ типа Throwable (подробнее о возвращаемых ошибках в [документации][full-doc]) ### Система быстрых платежей -Включение причема платежей через Систему быстрых платежей осуществляется в Личном кабинете. -#### Включение приема оплаты через СБП по кнопке для покупателя: -При конфигурировании параметров экрана оплаты, необходимо передать соответствующий параметр в featuresOptions. По умолчанию Система быстрых платежей в SDK отключена. - -```kotlin -var paymentOptions = PaymentOptions().setOptions { - orderOptions { /*options*/ } - customerOptions { /*options*/ } - featuresOptions { - fpsEnabled = true - } -} -``` - +Включение приема платежей через Систему быстрых платежей осуществляется в Личном кабинете. #### Прием оплаты по статическому QR коду через СБП Чтобы реализовать оплату с помощью статического QR кода на экране приложения, необходимо: -1) Создать соответствующую кнопку приема оплаты в приложении кассира -2) Установить слушатель на клик по кнопке и вызвать в нем метод **TinkoffAcquiring**#_openStaticQrScreen_ +1. Создать соответствующую кнопку приема оплаты в приложении кассира +2. Установить слушатель на клик по кнопке и вызвать в нем метод **TinkoffAcquiring**#_openStaticQrScreen_ Метод openStaticQrScreen принимает параметры: activity, localization - для локализации сообщения на экране, requestCode - для получения ошибки, если таковая возникнет. Результат оплаты товара покупателем по статическому QR коду не отслеживается в SDK, соответственно в onActivityResult вызывающего экран активити может вернуться только ошибка или отмена (закрытие экрана). + ### Tinkoff Pay Включение приема платежей через Tinkoff Pay осуществляется в Личном кабинете. #### Включение приема оплаты через Tinkoff Pay по кнопке для покупателя: -При инициализации экрана оплаты SDK проверит наличие возможности оплаты через Tinkoff Pay и в зависимости от результата отобразит -кнопку оплаты. Отключить отображение кнопки программно можно с помощью параметра `tinkoffPayEnabled` в `featuresOptions`. +Оплату с помощью Tinkoff Pay по кнопке можно настроить двумя способами +1. с использованием экранов из SDK; +2. с использованием своих экранов и `TpayProcess` из SDK. + +Оба варианта содержат общие шаги описанные ниже: +1. Для начала необходимо вызвать метод `TinkoffAcquiring.checkTerminalInfo`, в котором можно проверить доступность метода оплаты с помощью расширения `enableTinkoffPay()`. +2. Если `enableTinkoffPay()` возвращает `true`, то можно отобразить кнопку оплаты Tinkoff Pay в вашем приложении в соответствии с Design Guidelines. +##### Использование готовых экранов SDK +3. По нажатию на кнопку инициализировать платежную сессию и запустить экран оплаты SDK: ```kotlin -var paymentOptions = PaymentOptions().setOptions { - orderOptions { /*options*/ } - customerOptions { /*options*/ } - featuresOptions { - tinkoffPayEnabled = false // отключение отображения кнопки оплаты через Tinkoff Pay; по умолчанию отображение включено - } -} +TinkoffAcquiring.initTinkoffPayPaymentSession() + +val tpayPayment = registerForActivityResult(TpayLauncher.Contract, ActivityResultCallback {}) + +// настраиваются по аналогии с экраном оплаты +val paymentOptions = PaymentOptions() + .setOptions { + orderOptions { ... } + customerOptions { ... } + featuresOptions { ... } + setTerminalParams("TERMINAL_KEY", "PUBLIC_KEY") + } + +tpayPayment.launch( + StartData( + paymentOptions, + version // берется из ответа метода checkTerminalInfo() + ) +) ``` +4. Результат оплаты вернется в **ActivityResultCallback** в виде **TpayLauncher.Result** -Для определения возможности оплаты через Tinkoff Pay SDK посылает запрос на "https://securepay.tinkoff.ru/v2/TinkoffPay/terminals/$terminalKey/status". -Результат выполнения запроса кэшируется в SDK на период в 5 минут для уменьшения количества исходящих запросов. +### Mir Pay +Включение и прием платежей через Mir Pay осуществляется по аналогии с Tinkoff Pay, только для этого используются `MirPayLauncher` или `MirPayProcess` -Для отображения кнопки оплаты через Tinkoff Pay внутри вашего приложения (вне экрана оплаты, предоставляемого SDK) необходимо: -1. Самостоятельно вызвать метод определения доступности оплаты через Tinkoff Pay. Для этого можно использовать метод `TinkoffAcquiring.checkTinkoffPayStatus` -2. При наличии возможности оплаты отобразить кнопку оплаты через Tinkoff Pay в вашем приложении в соответствии с Design Guidelines -3. По нажатию на кнопку создать процесс оплаты с помощью метода `TinkoffAcquiring.payWithTinkoffPay` (параметр `version` можно получить -из ответа на шаге 1), зарегистрировать в нем слушатель событий (c обработкой состояния `OpenTinkoffPayBankState` в методе `onUiNeeded` и -использующий `state.deepLink` для открытия приложения с формой оплаты) и запустить процесс оплаты (метод `start()`) -4. При необходимости, проверить статус платежа при помощи `TinkoffAcquiring.sdk.getState` (с указанием `paymentId` полученном в `state.paymentId` на -предыдущем шаге); время и частота проверки статуса платежа зависит от нужд клиентского приложения и остается на ваше усмотрение (один из вариантов - -проверять статус платежа при возвращении приложения из фона) +##### Использование своих экранов и `TpayProcess` из SDK +3. По нажатию на кнопку инициализировать платежную сессию `TinkoffAcquiring.initTinkoffPayPaymentSession()` +4. Запусить свою реализацию экрана оплаты в которой необходимо получить экземпляр `TpayProcess#get` и стартовать процесс `TpayProcess#start` (параметр `version` можно получить из ответа на шаге 1. Отслеживать статус процесса оплаты можно через поле `TpayProcess#state`(под капотом используются корутины, если вы используете что-то другое, воспользуйтесь адаптером), обработать событие `onUiNeeded` и +использовать `state.deepLink` для открытия приложения с формой оплаты. +5. При необходимости, проверить статус платежа при помощи `TinkoffAcquiring.sdk.getState` (с указанием `paymentId` полученном в `state.paymentId` на предыдущем шаге); время и частота проверки статуса платежа зависит от нужд клиентского приложения и остается на ваше усмотрение (один из вариантов - проверять статус платежа при возвращении приложения из фона). ### Yandex Pay AcquiringSdk имеет возможность использовать внутри себя Yandex Pay в качестве хранилища карт. -Внимание, этот функционал поддерживается только с версии Android 6.0 Marshmallow и выше. Если вы хотите использовать Yandex Pay вместе с AcquiringSdk вам необходимо: 1. Получить в личном кабинете [Yandex](https://pay.yandex.ru/ru/docs/psp/android-sdk) значение `YANDEX_CLIENT_ID` @@ -242,50 +267,28 @@ implementation 'ru.tinkoff.acquiring:yandexpay:$latestVersion' ### Дополнительные возможности -#### Настройка стилей -В приложении есть базовая тема AcquiringTheme. Для изменения темы необходимо переопределить атрибуты. -На всех экранах SDK используется одна и та же тема. - #### Локализация -SDK имеет поддержку настройки локализации интерфейса, которая может не зависеть от локали устройства. -В SDK реализовано 2 языковые локализации - русская и английская. Чтобы установить локализацию из SDK, необходимо передать в параметр настройки экрана объект AsdkSource и указать его параметр Language.RU или Language.EN. - -```kotlin -var paymentOptions = PaymentOptions().setOptions { - orderOptions { /*options*/ } - customerOptions { /*options*/ } - featuresOptions { - localizationSource = AsdkSource(Language.RU) - } -} -``` - -По умолчанию, если в localizationSource не задан, определяется и используется локаль устройства. -Файлы локализации имеют формат json, где ключом является место использования строки на форме, значением - перевод. -Существует возможность добавить свои локализации на другие языки. Подробнее см. [файл полной документации][full-doc]. +SDK имеет поддержку 2 локализаций, русскую и английскую. #### Проведение платежа без открытия экрана оплаты -Для проведения платежа без открытия экрана необходимо вызвать метод **TinkoffAcquiring**#_initPayment_, который запустит процесс оплаты с инициацией и подтверждением платежа (будут вызваны методы API Init и FinishAuthorize). -В параметры метода необходимо передать карточные данные для оплаты и параметры платежа PaymentOptions -За выполнение методов отвечает объект SDK PaymentProcess, который имеет возможность уведомлять слушателя о событиях в процессе выполнения, методы подписки и отписки от событий, а также методы старта и остановки процесса. -Методы выполняются асинхронно. +Для проведения платежа без открытия экрана необходимо создать требуемый процесс для оплаты, передать параметры и написать свою логику обработки состояний платежа. +Для разных способов оплаты существуют разные бизнес сущности процесса оплаты, и разный набор состояний, они лежат в папке `ru.tinkoff.acquiring.sdk.payment` Пример запуска платежа: ```kotlin -TinkoffAcquiring.initPayment(token, paymentOptions) // создание процесса платежа - .subscribe(paymentListener) // подписка на события процесса - .start() // запуск процесса +PaymentByCardProcess.init(sdk, application) // создание процесса платежа +val process = PaymentByCardProcess.get() +process.start(cardData, paymentOptions) // запуск процесса +scope.launch { + process.state.collect { handle(it) } // подписка на события процесса +} ``` +Более подробные варианты использования можно посмотреть в sample проекта. + #### Завершение оплаты с уже существующим paymentId Для отображения платежной формы и проведения платежа без вызова метода Init можно передать -значение `SelectCardAndPayState` при вызове `openPaymentScreen`, пример вызова: -```kotlin -val paymentId = 123456789L // некоторый paymentId, полученный ранее при вызове метода Init -tinkoffAcquiring.openPaymentScreen(this@MainActivity, paymentOptions, PAYMENT_REQUEST_CODE, SelectCardAndPayState(paymentId)) -``` - -Для завершения платежа без отображения платежной формы можно использовать метод `TinkoffAcquiring.finishPayment`. +значение `paymentId` в соответствующие `Launcher`-ы экранов или если не требуется UI, то в `Process`-ы оплаты. ### Структура SDK состоит из следующих модулей: @@ -301,19 +304,24 @@ implementation 'ru.tinkoff.acquiring:core:$latestVersion' ``` #### UI -Содержит интерфейс, необходимый для приема платежей через мобильное приложение. +Содержит классы необходимые для приема платежей через мобильное приложение. -Основной класс - **TinkoffAcquiring** - позволяет: -* открывать экран совершения платежа -* открывать экран привязки карты +**TinkoffAcquiring** - позволяет: * открывать экран оплаты по статическому QR коду -* проводить полную сессию платежа без открытия экранов, с передачей платежных данных -* проводить только подтверждение платежа без открытия экранов, с передачей платежных данных -* настроить экран для приема оплаты из уведомления +* проверять доступность методов оплаты + +**TPayLauncher** - позволяет открывать экран процесса оплаты через Tinkoff Pay +**TpayProcess** - можно использовать для проведения процесса оплаты используя свой UI + +а так же аналогичные классы для возможности проведения оплаты. + #### Card-IO Модуль для сканирования карты камерой телефона с помощью библиотеки Card-IO. +#### Yandex +Модуль для работы с библиотекой yandexPay + #### Sample Содержит пример интеграции Tinkoff Acquiring SDK и модуля сканирования Card-IO в мобильное приложение по продаже книг. @@ -331,7 +339,7 @@ implementation 'ru.tinkoff.acquiring:core:$latestVersion' ### Поддержка - По возникающим вопросам просьба обращаться на [oplata@tinkoff.ru][support-email] - Баги и feature-реквесты можно направлять в раздел [issues][issues] -- Документация на [GitHub Pages](https://tinkoff.github.io/AcquiringSdkAndroid/ui/ru.tinkoff.acquiring.sdk/-tinkoff-acquiring/index.html) +- Полная документация по методам [api][full-doc] [search.maven]: http://search.maven.org/#search|ga|1|ru.tinkoff.acquiring.ui [build-config]: https://developer.android.com/studio/build/index.html diff --git a/sample/build.gradle b/sample/build.gradle index 0e5da7df..fb7db9aa 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -27,8 +27,12 @@ android { ] } buildTypes { + debug { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro index f1b42451..8349b9a9 100644 --- a/sample/proguard-rules.pro +++ b/sample/proguard-rules.pro @@ -19,3 +19,11 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-keep class ru.tinkoff.acquiring.sdk.localization.** { *; } +-keep class ru.tinkoff.acquiring.sdk.requests.** { *; } +-keep class ru.tinkoff.acquiring.sdk.responses.** { *; } +-keep class ru.tinkoff.acquiring.sdk.models.** { *; } +-keep class ru.tinkoff.acquiring.sdk.yandexpay.models.** { *; } +-keep class ru.rtln.tds.sdk.** { *; } +-keep class org.spongycastle.** +-keep class org.bouncycastle.** diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ca8db8c7..ef45f559 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme" + android:largeHeap="true" tools:ignore="GoogleAppIndexingWarning"> - - 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 60a2acb1..59cce6d6 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/SampleApplication.kt @@ -23,7 +23,6 @@ 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 -import ru.tinkoff.acquiring.sdk.payment.PaymentProcess import ru.tinkoff.acquiring.sdk.utils.SampleAcquiringTokenGenerator /** @@ -40,20 +39,17 @@ class SampleApplication : Application() { AcquiringSdk.customUrl = SettingsSdkManager(this).customUrl } - override fun onTerminate() { - super.onTerminate() - paymentProcess?.stop() - } - companion object { lateinit var tinkoffAcquiring: TinkoffAcquiring private set - var paymentProcess: PaymentProcess? = null fun initSdk(context: Context, params: SessionParams) { - tinkoffAcquiring = TinkoffAcquiring(context.applicationContext, - params.terminalKey, params.publicKey) + tinkoffAcquiring = TinkoffAcquiring( + context.applicationContext, + params.terminalKey, + params.publicKey + ) AcquiringSdk.tokenGenerator = params.password?.let { SampleAcquiringTokenGenerator(it) } } } -} \ No newline at end of file +} diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/camera/DemoCameraScanActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/camera/DemoCameraScanActivity.kt index 90ee4e13..462f7de5 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/camera/DemoCameraScanActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/camera/DemoCameraScanActivity.kt @@ -17,11 +17,15 @@ package ru.tinkoff.acquiring.sample.camera import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Button import androidx.appcompat.app.AppCompatActivity import ru.tinkoff.acquiring.sample.R +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.CardScannerContract +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.ScannedCardResult +import ru.tinkoff.acquiring.sdk.cardscanners.models.AsdkScannedCardData import java.util.* /** @@ -52,8 +56,33 @@ class DemoCameraScanActivity : AppCompatActivity() { const val EXTRA_CARD_NUMBER = "card_number" const val EXTRA_EXPIRE_DATE = "expire_date" - private const val EXPIRE_DATE = "11/21" - private val CARD_NUMBERS = arrayOf("5136 9149 2034 4072", "5136 9149 2034 7240", "5203 7375 0075 0535", "5203 7375 0075 3505") + private const val EXPIRE_DATE = "11/28" + private val CARD_NUMBERS = arrayOf( + "5136 9149 2034 4072", + "5136 9149 2034 7240", + "5203 7375 0075 0535", + "5203 7375 0075 3505" + ) private val random = Random() } + + object Contract : CardScannerContract() { + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(context, DemoCameraScanActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?): ScannedCardResult { + return when (resultCode) { + RESULT_CANCELED -> ScannedCardResult.Cancel + RESULT_OK -> ScannedCardResult.Success( + AsdkScannedCardData( + cardNumber = intent?.getStringExtra(EXTRA_CARD_NUMBER).orEmpty(), + expireDate = intent?.getStringExtra(EXTRA_EXPIRE_DATE).orEmpty(), + "" + ) + ) + else -> ScannedCardResult.Failure(null) + } + } + } } \ No newline at end of file diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PaymentNotificationIntentService.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PaymentNotificationIntentService.kt deleted file mode 100644 index de3caaa9..00000000 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PaymentNotificationIntentService.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.sample.service - -import android.app.IntentService -import android.content.Intent -import ru.tinkoff.acquiring.sample.utils.PaymentNotificationManager.ACTION_SELECT_PRICE - -/** - * @author Mariya Chernyadieva - */ -class PaymentNotificationIntentService : IntentService("PaymentNotificationIntentService") { - - override fun onHandleIntent(intent: Intent?) { - intent?.let { - val intentAction = it.action - if (intentAction != null && intentAction.startsWith(ACTION_SELECT_PRICE)) { - val option = intentAction.substringAfter(ACTION_SELECT_PRICE) - val responseIntent = Intent().apply { - action = ACTION_PRICE_SELECT - addCategory(Intent.CATEGORY_DEFAULT) - putExtra(EXTRA_NOTIFICATION_PRICE_OPTION, option) - } - sendBroadcast(responseIntent) - } - } - } - - companion object { - const val ACTION_PRICE_SELECT = "ru.tinkoff.acquiring.sample.service.PRICE_SELECT" - const val EXTRA_NOTIFICATION_PRICE_OPTION = "price_option" - } - -} diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PriceNotificationReceiver.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PriceNotificationReceiver.kt deleted file mode 100644 index 20336d6d..00000000 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/service/PriceNotificationReceiver.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.sample.service - -import android.app.Activity -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import ru.tinkoff.acquiring.sample.utils.PaymentNotificationManager - -/** - * @author Mariya Chernyadieva - */ -class PriceNotificationReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent) { - intent.getStringExtra(PaymentNotificationIntentService.EXTRA_NOTIFICATION_PRICE_OPTION)?.let { - PaymentNotificationManager.triggerNotification(context as Activity, it) - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/AcqEnvironmentDialog.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/AcqEnvironmentDialog.kt new file mode 100644 index 00000000..2f859270 --- /dev/null +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/AcqEnvironmentDialog.kt @@ -0,0 +1,64 @@ +package ru.tinkoff.acquiring.sample.ui + +import android.os.Bundle +import android.view.* +import android.widget.LinearLayout +import android.widget.Switch +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 + + +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 isPreProdSwitcher: Switch by lazy { + requireView().findViewById(R.id.acq_env_is_pre_prod) + } + + 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) + + isPreProdSwitcher.setOnCheckedChangeListener { _, isPreprod -> + setEnv(isPreprod) + } + + setEnv(AcquiringSdk.isPreprodMode) + isPreProdSwitcher.isChecked = AcquiringSdk.isPreprodMode + + ok.setOnClickListener { dismiss() } + } + + override fun onResume() { + dialog?.window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + super.onResume() + } + + private fun getDescriptionText(): String { + return "Для запросов на Back-end Acquiring будет использован URL: ${AcquiringApi.getUrl("")} " + } + + private fun setEnv(isPreprod: Boolean) { + AcquiringSdk.isPreprodMode = isPreprod + description.text = getDescriptionText() + } + + companion object { + const val TAG = "AcqEnvironmentDialog" + } +} diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/CartActivity.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/CartActivity.kt index 69d1e820..194f79c9 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/CartActivity.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/ui/CartActivity.kt @@ -26,6 +26,7 @@ import android.widget.ArrayAdapter import android.widget.LinearLayout import android.widget.ListView import android.widget.TextView +import androidx.core.view.isVisible import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.adapters.CartListAdapter import ru.tinkoff.acquiring.sample.models.BooksRegistry @@ -42,6 +43,7 @@ class CartActivity : PayableActivity(), CartListAdapter.DeleteCartItemListener { private lateinit var listViewCartItems: ListView private lateinit var cartContentLayout: LinearLayout private lateinit var buttonPay: TextView + private lateinit var recurrentButton: TextView private var cartEmpty: Boolean = true private var adapter: ArrayAdapter<*>? = null @@ -69,9 +71,13 @@ class CartActivity : PayableActivity(), CartListAdapter.DeleteCartItemListener { setupTinkoffPay() + setupMirPay() + checkCartEmpty() setupYandexPay(R.style.AcquiringTheme_Base_Yandex,savedInstanceState) + + setupRecurrentParentPayment() } override fun onResume() { @@ -160,6 +166,10 @@ class CartActivity : PayableActivity(), CartListAdapter.DeleteCartItemListener { return result.toString() } + private fun setupRecurrentParentPayment(hasRecurrent: Boolean) { + recurrentButton.isVisible = hasRecurrent + } + companion object { fun start(context: Context) { 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 c1670cab..f6cacfd1 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 @@ -26,7 +26,6 @@ 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 @@ -37,9 +36,8 @@ 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.sdk.payment.methods.InitConfigurator.configure import ru.tinkoff.acquiring.yandexpay.models.YandexPayData import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest import ru.tinkoff.acquiring.yandexpay.* @@ -59,11 +57,6 @@ 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) @@ -93,11 +86,7 @@ class DetailsActivity : PayableActivity() { val buttonBuy = findViewById(R.id.btn_buy_now) buttonBuy.setOnClickListener { - //Стандартный метод проведения оплаты с получением результата в OnActivityResult - //initPayment() - - //Метод проведения оплаты с получением результата в ActivityResultAPI - initActivityResultAPIPayment() + initPayment() } val sbpButton = findViewById(R.id.btn_fps_pay) @@ -108,17 +97,13 @@ class DetailsActivity : PayableActivity() { setupTinkoffPay() + setupMirPay() + setupYandexPay(savedInstanceState = savedInstanceState) 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) @@ -150,10 +135,8 @@ class DetailsActivity : PayableActivity() { ): YandexButtonFragment { return savedInstanceState?.let { try { - (supportFragmentManager.getFragment( - savedInstanceState, - YANDEX_PAY_FRAGMENT_KEY - ) as? YandexButtonFragment)?.also { + val yaFragment = (supportFragmentManager.getFragment(savedInstanceState, YANDEX_PAY_FRAGMENT_KEY) as? YandexButtonFragment) + yaFragment?.also { tinkoffAcquiring.addYandexResultListener( fragment = it, activity = this, @@ -209,8 +192,10 @@ class DetailsActivity : PayableActivity() { ) } catch (e: java.lang.Exception) { if (e !is CancellationException) { - hideProgressDialog() - showErrorDialog() + runOnUiThread { + hideProgressDialog() + showErrorDialog() + } } } } 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 af4d57ac..bd665fb2 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 @@ -18,33 +18,28 @@ package ru.tinkoff.acquiring.sample.ui import android.app.Activity import android.content.Intent -import android.content.IntentFilter -import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.widget.ListView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.SampleApplication import ru.tinkoff.acquiring.sample.adapters.BooksListAdapter 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.SettingsSdkManager import ru.tinkoff.acquiring.sample.utils.TerminalsManager -import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.EXTRA_CARD_ID -import ru.tinkoff.acquiring.sdk.TinkoffAcquiring.Companion.RESULT_ERROR import ru.tinkoff.acquiring.sdk.localization.AsdkSource import ru.tinkoff.acquiring.sdk.localization.Language import ru.tinkoff.acquiring.sdk.models.options.FeaturesOptions -import ru.tinkoff.acquiring.sdk.models.options.screen.AttachCardOptions -import ru.tinkoff.acquiring.sdk.models.options.screen.SavedCardsOptions import ru.tinkoff.acquiring.sdk.models.result.CardResult +import ru.tinkoff.acquiring.sdk.redesign.cards.attach.AttachCardLauncher +import ru.tinkoff.acquiring.sdk.redesign.cards.list.SavedCardsLauncher +import ru.tinkoff.acquiring.sdk.redesign.common.LauncherConstants.RESULT_ERROR import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper /** @@ -55,9 +50,24 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe private lateinit var listViewBooks: ListView private lateinit var adapter: BooksListAdapter private lateinit var settings: SettingsSdkManager - private val priceNotificationReceiver = PriceNotificationReceiver() private var selectedCardIdForDemo: String? = null + private val attachCard = registerForActivityResult(AttachCardLauncher.Contract) { result -> + when (result) { + is AttachCardLauncher.Success -> PaymentResultActivity.start(this, result.cardId) + is AttachCardLauncher.Error -> toast(result.error.message ?: getString(R.string.attachment_failed)) + is AttachCardLauncher.Canceled -> toast(R.string.attachment_cancelled) + } + } + + private val savedCards = registerForActivityResult(SavedCardsLauncher.Contract) { result -> + when (result) { + is SavedCardsLauncher.Success -> selectedCardIdForDemo = result.selectedCardId + is SavedCardsLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + else -> Unit + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,17 +75,9 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe settings = SettingsSdkManager(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PaymentNotificationManager.createNotificationChannel(this) - } - val booksRegistry = BooksRegistry() adapter = BooksListAdapter(this, booksRegistry.getBooks(this), this) initViews() - - val intentFilter = IntentFilter(PaymentNotificationIntentService.ACTION_PRICE_SELECT) - intentFilter.addCategory(Intent.CATEGORY_DEFAULT) - registerReceiver(priceNotificationReceiver, intentFilter) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -92,11 +94,6 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe invalidateOptionsMenu() } - override fun onDestroy() { - super.onDestroy() - unregisterReceiver(priceNotificationReceiver) - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_action_cart -> { @@ -132,15 +129,14 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe openStaticQrScreen() true } - R.id.menu_action_send_notification -> { - PaymentNotificationManager.triggerNotification(this, - PaymentNotificationManager.PRICE_BUTTON_ID_2) - true - } R.id.menu_action_settings -> { SettingsActivity.start(this) true } + R.id.menu_action_environment -> { + AcqEnvironmentDialog().show(supportFragmentManager, AcqEnvironmentDialog.TAG) + true + } else -> super.onOptionsItemSelected(item) } } @@ -152,27 +148,6 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe Toast.makeText(this, R.string.payment_failed, Toast.LENGTH_SHORT).show() } } - ATTACH_CARD_REQUEST_CODE -> { - when (resultCode) { - RESULT_OK -> PaymentResultActivity.start(this, - data?.getStringExtra(EXTRA_CARD_ID)!!) - RESULT_CANCELED -> Toast.makeText(this, - R.string.attachment_cancelled, - Toast.LENGTH_SHORT).show() - RESULT_ERROR -> Toast.makeText(this, - R.string.attachment_failed, - Toast.LENGTH_SHORT).show() - } - } - SAVED_CARDS_REQUEST_CODE -> { - val selectedCardId = data?.getStringExtra(EXTRA_CARD_ID) - selectedCardIdForDemo = selectedCardId - - when (resultCode) { - RESULT_OK -> Toast.makeText(this, "Выбранная карта: CardId=$selectedCardId", Toast.LENGTH_SHORT).show() - RESULT_ERROR -> Toast.makeText(this, R.string.error_title, Toast.LENGTH_SHORT).show() - } - } NOTIFICATION_PAYMENT_REQUEST_CODE -> { when (resultCode) { RESULT_OK -> { @@ -192,7 +167,7 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe when (resultCode) { RESULT_OK -> { val result = data?.getSerializableExtra(ThreeDsHelper.Launch.RESULT_DATA) as CardResult - toast("Attach success: cardId = ${result.cardId} ") + toast("Attach success card: ${result.panSuffix ?: result.cardId}") } RESULT_CANCELED -> toast("Attach canceled") RESULT_ERROR -> { @@ -219,25 +194,23 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe val settings = SettingsSdkManager(this) val params = TerminalsManager.selectedTerminal - val options = AttachCardOptions() - .setOptions { - customerOptions { - customerKey = params.customerKey - checkType = settings.checkType - email = params.customerEmail - } - featuresOptions { - useSecureKeyboard = settings.isCustomKeyboardEnabled - validateExpiryDate = settings.validateExpiryDate - cameraCardScanner = settings.cameraScanner - darkThemeMode = settings.resolveDarkThemeMode() - theme = settings.resolveAttachCardStyle() - } - } + val options = SampleApplication.tinkoffAcquiring.attachCardOptions { + customerOptions { + customerKey = params.customerKey + checkType = settings.checkType + email = params.customerEmail + } + featuresOptions { + useSecureKeyboard = settings.isCustomKeyboardEnabled + validateExpiryDate = settings.validateExpiryDate + cameraCardScanner = settings.cameraScanner + cameraCardScannerContract = settings.cameraScannerContract + darkThemeMode = settings.resolveDarkThemeMode() + theme = settings.resolveAttachCardStyle() + } + } - SampleApplication.tinkoffAcquiring.openAttachCardScreen(this, - options, - ATTACH_CARD_REQUEST_CODE) + attachCard.launch(options) } private fun openStaticQrScreen() { @@ -254,7 +227,7 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe val settings = SettingsSdkManager(this) val params = TerminalsManager.selectedTerminal - val options = SavedCardsOptions().setOptions { + savedCards.launch(SampleApplication.tinkoffAcquiring.savedCardsOptions { customerOptions { customerKey = params.customerKey checkType = settings.checkType @@ -264,28 +237,27 @@ class MainActivity : AppCompatActivity(), BooksListAdapter.BookDetailsClickListe useSecureKeyboard = settings.isCustomKeyboardEnabled validateExpiryDate = settings.validateExpiryDate cameraCardScanner = settings.cameraScanner + cameraCardScannerContract = settings.cameraScannerContract darkThemeMode = settings.resolveDarkThemeMode() theme = settings.resolveAttachCardStyle() userCanSelectCard = true selectedCardId = selectedCardIdForDemo } - } - - SampleApplication.tinkoffAcquiring.openSavedCardsScreen(this, - options, - SAVED_CARDS_REQUEST_CODE) + }) } companion object { - private const val ATTACH_CARD_REQUEST_CODE = 11 private const val STATIC_QR_REQUEST_CODE = 12 - private const val SAVED_CARDS_REQUEST_CODE = 13 const val NOTIFICATION_PAYMENT_REQUEST_CODE = 14 const val THREE_DS_REQUEST_CODE = 15 fun Activity.toast(message: String) = runOnUiThread { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } + + fun Activity.toast(@StringRes message: Int) = runOnUiThread { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } } } 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 fc8f0b35..55eb254f 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,32 +18,46 @@ 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.TextView 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 kotlinx.coroutines.Dispatchers +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import ru.tinkoff.acquiring.sample.R import ru.tinkoff.acquiring.sample.SampleApplication +import ru.tinkoff.acquiring.sample.ui.MainActivity.Companion.toast +import ru.tinkoff.acquiring.sample.utils.CombInitDelegate 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.Card 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.models.options.screen.SavedCardsOptions +import ru.tinkoff.acquiring.sdk.payment.* +import ru.tinkoff.acquiring.sdk.redesign.cards.list.ChooseCardLauncher +import ru.tinkoff.acquiring.sdk.redesign.common.LauncherConstants.EXTRA_ERROR +import ru.tinkoff.acquiring.sdk.redesign.common.LauncherConstants.EXTRA_PAYMENT_ID +import ru.tinkoff.acquiring.sdk.redesign.common.LauncherConstants.RESULT_ERROR +import ru.tinkoff.acquiring.sdk.threeds.ThreeDsHelper +import ru.tinkoff.acquiring.sdk.redesign.mainform.MainFormLauncher +import ru.tinkoff.acquiring.sdk.redesign.mirpay.MirPayLauncher +import ru.tinkoff.acquiring.sdk.redesign.payment.PaymentByCardLauncher +import ru.tinkoff.acquiring.sdk.redesign.recurrent.RecurrentPayLauncher +import ru.tinkoff.acquiring.sdk.redesign.sbp.SbpPayLauncher +import ru.tinkoff.acquiring.sdk.redesign.tpay.TpayLauncher +import ru.tinkoff.acquiring.sdk.redesign.tpay.models.enableMirPay +import ru.tinkoff.acquiring.sdk.redesign.tpay.models.enableTinkoffPay +import ru.tinkoff.acquiring.sdk.redesign.tpay.models.getTinkoffPayVersion import ru.tinkoff.acquiring.sdk.utils.Money import ru.tinkoff.acquiring.sdk.utils.getLongOrNull import ru.tinkoff.acquiring.yandexpay.YandexButtonFragment @@ -53,6 +67,7 @@ import ru.tinkoff.acquiring.yandexpay.models.YandexPayData import ru.tinkoff.acquiring.yandexpay.models.enableYandexPay import ru.tinkoff.acquiring.yandexpay.models.mapYandexPayData import java.util.* +import kotlin.collections.ArrayList import kotlin.math.abs /** @@ -68,13 +83,70 @@ open class PayableActivity : AppCompatActivity() { private lateinit var progressDialog: AlertDialog private var errorDialog: AlertDialog? = null - private val paymentListener = createPaymentListener() private var isProgressShowing = false private var isErrorShowing = false protected var tinkoffAcquiring = SampleApplication.tinkoffAcquiring private val orderId: String get() = abs(Random().nextInt()).toString() private var acqFragment: YandexButtonFragment? = null + private val combInitDelegate: CombInitDelegate = CombInitDelegate(tinkoffAcquiring.sdk, Dispatchers.IO) + private val byCardPayment = registerForActivityResult(PaymentByCardLauncher.Contract) { result -> + when (result) { + is PaymentByCardLauncher.Success -> { + toast("byCardPayment Success : ${result.paymentId}") + } + is PaymentByCardLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is PaymentByCardLauncher.Canceled -> toast("byCardPayment canceled") + } + } + private val spbPayment = registerForActivityResult(SbpPayLauncher.Contract) { result -> + when (result) { + is SbpPayLauncher.Success -> { + toast("SBP Success") + } + is SbpPayLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is SbpPayLauncher.Canceled -> toast("SBP canceled") + is SbpPayLauncher.NoBanks -> Unit + } + } + private val byMainFormPayment = registerForActivityResult(MainFormLauncher.Contract) { result -> + when (result) { + is MainFormLauncher.Canceled -> toast("payment canceled") + is MainFormLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is MainFormLauncher.Success -> toast("payment Success- paymentId:${result.paymentId}") + } + } + private val recurrentPayment = registerForActivityResult(RecurrentPayLauncher.Contract) { result -> + when (result) { + is RecurrentPayLauncher.Canceled -> toast("payment canceled") + is RecurrentPayLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is RecurrentPayLauncher.Success -> toast("payment Success- paymentId:${result.paymentId}") + } + } + private val cardsForRecurrent = + registerForActivityResult(ChooseCardLauncher.Contract) { result -> + when (result) { + is ChooseCardLauncher.Canceled -> Unit + is ChooseCardLauncher.Error -> Unit + is ChooseCardLauncher.Success -> launchRecurrent(result.card) + is ChooseCardLauncher.NeedInputNewCard -> Unit + } + } + + private val tpayPayment = registerForActivityResult(TpayLauncher.Contract) { result -> + when (result) { + is TpayLauncher.Canceled -> toast("tpay canceled") + is TpayLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is TpayLauncher.Success -> toast("payment Success- paymentId:${result.paymentId}") + } + } + private val mirPayment = registerForActivityResult(MirPayLauncher.Contract) { result -> + when (result) { + is MirPayLauncher.Canceled -> toast("MirPay canceled") + is MirPayLauncher.Error -> toast(result.error.message ?: getString(R.string.error_title)) + is MirPayLauncher.Success -> toast("payment Success- paymentId:${result.paymentId}") + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -89,13 +161,10 @@ open class PayableActivity : AppCompatActivity() { settings = SettingsSdkManager(this) initDialogs() - - SampleApplication.paymentProcess?.subscribe(paymentListener) } override fun onDestroy() { super.onDestroy() - SampleApplication.paymentProcess?.unsubscribe() if (progressDialog.isShowing) { progressDialog.dismiss() } @@ -134,15 +203,22 @@ open class PayableActivity : AppCompatActivity() { protected open fun onSuccessPayment() { PaymentResultActivity.start(this, totalPrice) - SampleApplication.paymentProcess = null } protected fun initPayment() { - tinkoffAcquiring.openPaymentScreen(this, createPaymentOptions(), PAYMENT_REQUEST_CODE) - } - - protected fun getPaymentPendingIntent(): PendingIntent { - return tinkoffAcquiring.getPaymentPendingIntent(this, createPaymentOptions(), PAYMENT_REQUEST_CODE) + if (settings.isRecurrentPayment) { + RecurrentPaymentProcess.init(SampleApplication.tinkoffAcquiring.sdk, application, ThreeDsHelper.CollectData) + cardsForRecurrent.launch(createSavedCardOptions()) + } else { + val options = createPaymentOptions().apply { + this.setTerminalParams( + terminalKey = TerminalsManager.selectedTerminal.terminalKey, + publicKey = TerminalsManager.selectedTerminal.publicKey + ) + } + PaymentByCardProcess.init(SampleApplication.tinkoffAcquiring.sdk, application, ThreeDsHelper.CollectData) + byMainFormPayment.launch(MainFormLauncher.StartData(options)) + } } protected fun openDynamicQrScreen() { @@ -150,7 +226,29 @@ open class PayableActivity : AppCompatActivity() { } protected fun startSbpPayment() { - tinkoffAcquiring.payWithSbp(this, createPaymentOptions(), PAYMENT_REQUEST_CODE) + lifecycleScope.launch { + val opt = createPaymentOptions() + opt.setTerminalParams( + TerminalsManager.selectedTerminal.terminalKey, + TerminalsManager.selectedTerminal.publicKey + ) + if (settings.isEnableCombiInit) { + showProgressDialog() + runCatching { combInitDelegate.sendInit(opt).paymentId!! } + .onFailure { + hideProgressDialog() + showErrorDialog() + } + .onSuccess { + hideProgressDialog() + tinkoffAcquiring.initSbpPaymentSession() + spbPayment.launch(SbpPayLauncher.StartData(it, opt)) + } + } else { + tinkoffAcquiring.initSbpPaymentSession() + spbPayment.launch(SbpPayLauncher.StartData(opt)) + } + } } protected fun setupTinkoffPay() { @@ -158,16 +256,40 @@ open class PayableActivity : AppCompatActivity() { val tinkoffPayButton = findViewById(R.id.tinkoff_pay_button) - tinkoffAcquiring.checkTinkoffPayStatus({ status -> - if (!status.isTinkoffPayAvailable()) return@checkTinkoffPayStatus + tinkoffAcquiring.checkTerminalInfo({ status -> + if (status.enableTinkoffPay().not()) return@checkTerminalInfo tinkoffPayButton.visibility = View.VISIBLE - val version = status.getTinkoffPayVersion()!! + val opt = createPaymentOptions() + opt.setTerminalParams( + TerminalsManager.selectedTerminal.terminalKey, + TerminalsManager.selectedTerminal.publicKey + ) + val version = checkNotNull(status?.getTinkoffPayVersion()) + tinkoffAcquiring.initTinkoffPayPaymentSession() tinkoffPayButton.setOnClickListener { - tinkoffAcquiring.payWithTinkoffPay(createPaymentOptions(), version) - .subscribe(paymentListener) - .start() + tpayPayment.launch(TpayLauncher.StartData(opt, version)) + } + }) + } + + protected fun setupMirPay() { + val mirPayButton = findViewById(R.id.mir_pay_button) + + tinkoffAcquiring.checkTerminalInfo({ status -> + if (status.enableMirPay().not()) return@checkTerminalInfo + + mirPayButton.visibility = View.VISIBLE + + val opt = createPaymentOptions() + opt.setTerminalParams( + TerminalsManager.selectedTerminal.terminalKey, + TerminalsManager.selectedTerminal.publicKey + ) + tinkoffAcquiring.initMirPayPaymentSession() + mirPayButton.setOnClickListener { + mirPayment.launch(MirPayLauncher.StartData(opt)) } }) } @@ -184,7 +306,7 @@ open class PayableActivity : AppCompatActivity() { val paymentOptions = createPaymentOptions().apply { val session = TerminalsManager.init(this@PayableActivity).selectedTerminal this.setTerminalParams( - terminalKey = session.terminalKey, publicKey = session.publicKey + terminalKey = session.terminalKey, publicKey = session.publicKey ) } @@ -202,71 +324,82 @@ open class PayableActivity : AppCompatActivity() { }) } + protected fun setupRecurrentParentPayment() { + val recurrentButton : TextView? = findViewById(R.id.recurrent_pay) + recurrentButton?.isVisible = settings.isRecurrentPayment + recurrentButton?.setOnClickListener { + val paymentOptions = createPaymentOptions().apply { + val session = TerminalsManager.selectedTerminal + this.setTerminalParams( + terminalKey = session.terminalKey, publicKey = session.publicKey + ) + } + PaymentByCardProcess.init(SampleApplication.tinkoffAcquiring.sdk, application, ThreeDsHelper.CollectData) + byCardPayment.launch(PaymentByCardLauncher.StartData(paymentOptions, ArrayList())) + } + } + private fun createPaymentOptions(): PaymentOptions { val sessionParams = TerminalsManager.selectedTerminal return PaymentOptions() - .setOptions { - orderOptions { - orderId = this@PayableActivity.orderId - amount = totalPrice - title = this@PayableActivity.title - description = this@PayableActivity.description - recurrentPayment = settings.isRecurrentPayment - successURL = "https://www.google.com/search?q=success" - failURL = "https://www.google.com/search?q=fail" - additionalData = mutableMapOf( - "test_additional_data_key_1" to "test_additional_data_value_2", - "test_additional_data_key_2" to "test_additional_data_value_2") - } - customerOptions { - customerKey = sessionParams.customerKey - checkType = settings.checkType - email = sessionParams.customerEmail - } - featuresOptions { - localizationSource = AsdkSource(Language.RU) - handleCardListErrorInSdk = settings.handleCardListErrorInSdk - useSecureKeyboard = settings.isCustomKeyboardEnabled - validateExpiryDate = settings.validateExpiryDate - cameraCardScanner = settings.cameraScanner - fpsEnabled = settings.isFpsEnabled - tinkoffPayEnabled = settings.isTinkoffPayEnabled - darkThemeMode = settings.resolveDarkThemeMode() - theme = settings.resolvePaymentStyle() - userCanSelectCard = true - duplicateEmailToReceipt = true - } + .setOptions { + orderOptions { + orderId = this@PayableActivity.orderId + amount = totalPrice + title = this@PayableActivity.title + description = this@PayableActivity.description + recurrentPayment = settings.isRecurrentPayment + successURL = "https://www.google.com/search?q=success" + failURL = "https://www.google.com/search?q=fail" + additionalData = mutableMapOf( + "test_additional_data_key_1" to "test_additional_data_value_2", + "test_additional_data_key_2" to "test_additional_data_value_2" + ) } - } - - private fun createPaymentListener(): PaymentListener { - return object : PaymentListenerAdapter() { - - override fun onStatusChanged(state: PaymentState?) { - if (state == PaymentState.STARTED) { - showProgressDialog() + customerOptions { + customerKey = sessionParams.customerKey + checkType = settings.checkType + email = sessionParams.customerEmail + } + featuresOptions { + localizationSource = AsdkSource(Language.RU) + handleCardListErrorInSdk = settings.handleCardListErrorInSdk + useSecureKeyboard = settings.isCustomKeyboardEnabled + validateExpiryDate = settings.validateExpiryDate + cameraCardScanner = settings.cameraScanner + cameraCardScannerContract = settings.cameraScannerContract + fpsEnabled = settings.isFpsEnabled + tinkoffPayEnabled = settings.isTinkoffPayEnabled + darkThemeMode = settings.resolveDarkThemeMode() + theme = settings.resolvePaymentStyle() + userCanSelectCard = true + duplicateEmailToReceipt = true } } + } - override fun onSuccess(paymentId: Long, cardId: String?, rebillId: String?) { - hideProgressDialog() - onSuccessPayment() - } + private fun createSavedCardOptions(): SavedCardsOptions { + val settings = SettingsSdkManager(this) + val params = TerminalsManager.selectedTerminal - override fun onUiNeeded(state: AsdkState) { - hideProgressDialog() - tinkoffAcquiring.openPaymentScreen( - this@PayableActivity, - createPaymentOptions(), - PAYMENT_REQUEST_CODE, - state) + return SampleApplication.tinkoffAcquiring.savedCardsOptions { + withArrowBack = true + customerOptions { + customerKey = params.customerKey + checkType = settings.checkType + email = params.customerEmail } - - override fun onError(throwable: Throwable, paymentId: Long?) { - hideProgressDialog() - showErrorDialog() - SampleApplication.paymentProcess = null + featuresOptions { + useSecureKeyboard = settings.isCustomKeyboardEnabled + validateExpiryDate = settings.validateExpiryDate + cameraCardScanner = settings.cameraScanner + cameraCardScannerContract = settings.cameraScannerContract + darkThemeMode = settings.resolveDarkThemeMode() + theme = settings.resolveAttachCardStyle() + userCanSelectCard = true + selectedCardId = "" + showOnlyRecurrentCards = true } } } @@ -362,6 +495,32 @@ open class PayableActivity : AppCompatActivity() { isProgressShowing = false } + private fun getErrorFromIntent(data: Intent?): Throwable? { + return (data?.getSerializableExtra(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 launchRecurrent(card: Card) { + val options = createPaymentOptions().apply { + this.setTerminalParams( + terminalKey = TerminalsManager.selectedTerminal.terminalKey, + publicKey = TerminalsManager.selectedTerminal.publicKey + ) + } + recurrentPayment.launch( + RecurrentPayLauncher.StartData(card, options) + ) + } private fun commonErrorHandler(data: Intent?) { val error = getErrorFromIntent(data) @@ -372,9 +531,6 @@ open class PayableActivity : AppCompatActivity() { 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 @@ -389,6 +545,7 @@ open class PayableActivity : AppCompatActivity() { } } + 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 index ceb36a13..f152ee74 100644 --- 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 @@ -1,6 +1,7 @@ package ru.tinkoff.acquiring.sample.ui.environment +import android.annotation.SuppressLint import android.os.Bundle import android.view.* import android.widget.EditText @@ -8,17 +9,23 @@ 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 - +import ru.tinkoff.acquiring.sdk.utils.EnvironmentMode /** * Created by i.golovachev */ class AcqEnvironmentDialog : DialogFragment() { + companion object { + + private const val PRE_PROD_URL = "https://qa-mapi.tcsbank.ru" + const val TAG = "AcqEnvironmentDialog" + + } + private val ok: TextView by lazy { requireView().findViewById(R.id.acq_env_ok) } @@ -38,24 +45,18 @@ class AcqEnvironmentDialog : DialogFragment() { return inflater.inflate(R.layout.asdk_environment_dialog, container, false) } + @SuppressLint("SetTextI18n") 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_pre_prod_btn -> configureCustomUrl(PRE_PROD_URL, EnvironmentMode.IsPreProdMode) + R.id.acq_env_is_debug_btn -> configureCustomUrl(AcquiringApi.getUrl("/"), EnvironmentMode.IsDebugMode) R.id.acq_env_is_custom_btn -> { - customUrl = null + AcquiringSdk.environmentMode = EnvironmentMode.IsCustomMode editUrlText.setText("https://") + editUrlText.setSelection(editUrlText.text.length) editUrlText.isEnabled = true editUrlText.requestFocus() } @@ -71,6 +72,17 @@ class AcqEnvironmentDialog : DialogFragment() { } } + private fun configureCustomUrl(url: String, mode: EnvironmentMode) { + AcquiringSdk.environmentMode = mode + AcquiringSdk.customUrl = url + disableEditing(url) + } + + private fun disableEditing(url: String) = with(editUrlText) { + setText(url) + isEnabled = false + } + override fun onResume() { dialog?.window?.setLayout( LinearLayout.LayoutParams.MATCH_PARENT, @@ -79,34 +91,17 @@ class AcqEnvironmentDialog : DialogFragment() { 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) + when(AcquiringSdk.environmentMode) { + is EnvironmentMode.IsPreProdMode -> { + evnGroup.check(R.id.acq_env_is_pre_prod_btn) } - isDebug -> { + is EnvironmentMode.IsDebugMode -> { evnGroup.check(R.id.acq_env_is_debug_btn) - editUrlText.setText(AcquiringApi.getUrl("/")) } - else -> { - evnGroup.check(-1) + is EnvironmentMode.IsCustomMode -> { + evnGroup.check(R.id.acq_env_is_custom_btn) } } - - } - - 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/CombInitDelegate.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/CombInitDelegate.kt new file mode 100644 index 00000000..c236cedb --- /dev/null +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/CombInitDelegate.kt @@ -0,0 +1,29 @@ +package ru.tinkoff.acquiring.sample.utils + +import kotlinx.coroutines.* +import ru.tinkoff.acquiring.sdk.AcquiringSdk +import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions +import ru.tinkoff.acquiring.sdk.payment.methods.InitConfigurator.configure +import ru.tinkoff.acquiring.sdk.requests.performSuspendRequest +import ru.tinkoff.acquiring.sdk.responses.InitResponse +import kotlin.coroutines.CoroutineContext + + +/** + * Created by i.golovachev + * + * Имитирует запрос к строннему бекенду мерчанта, в рамках совершения комби-инит платежа + */ +class CombInitDelegate(private val sdk: AcquiringSdk, private val context: CoroutineContext) { + + private val combiInitAdditional = + mapOf("merchant init response field" to "merchant init response value") + + suspend fun sendInit(paymentOptions: PaymentOptions): InitResponse { + paymentOptions.order.additionalData = + paymentOptions.order.additionalData?.plus(combiInitAdditional) ?: combiInitAdditional + return withContext(context) { + sdk.init { configure(paymentOptions) }.performSuspendRequest().getOrThrow() + } + } +} diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/PaymentNotificationManager.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/PaymentNotificationManager.kt deleted file mode 100644 index d16c947b..00000000 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/PaymentNotificationManager.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.sample.utils - -import android.app.Activity -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.os.Build -import android.widget.RemoteViews -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import ru.tinkoff.acquiring.sample.R -import ru.tinkoff.acquiring.sample.SampleApplication -import ru.tinkoff.acquiring.sample.service.PaymentNotificationIntentService -import ru.tinkoff.acquiring.sample.ui.MainActivity.Companion.NOTIFICATION_PAYMENT_REQUEST_CODE -import ru.tinkoff.acquiring.sdk.localization.AsdkSource -import ru.tinkoff.acquiring.sdk.localization.Language -import ru.tinkoff.acquiring.sdk.models.GooglePayParams -import ru.tinkoff.acquiring.sdk.models.options.screen.PaymentOptions -import ru.tinkoff.acquiring.sdk.utils.Money -import java.util.Random -import kotlin.math.abs - -/** - * @author Mariya Chernyadieva - */ -object PaymentNotificationManager { - - const val ACTION_SELECT_PRICE = "SELECT_PRICE" - - const val PRICE_BUTTON_ID_1 = "button1" - const val PRICE_BUTTON_ID_2 = "button2" - const val PRICE_BUTTON_ID_3 = "button3" - - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_CHANNEL_ID = "payments_channel" - - private val priceMap: HashMap = hashMapOf( - PRICE_BUTTON_ID_1 to Money.ofRubles(100), - PRICE_BUTTON_ID_2 to Money.ofRubles(200), - PRICE_BUTTON_ID_3 to Money.ofRubles(300)) - - fun triggerNotification(activity: Activity, selectedButtonId: String) { - val res = activity.resources - val packageName = activity.packageName - - val notificationLayout = RemoteViews(packageName, R.layout.payment_notification) - - priceMap.keys.forEach { buttonPriceId -> - val buttonId = res.getIdentifier(buttonPriceId, "id", packageName) - val selectedBg = R.drawable.bg_notification_price_button_selected - val unselectedBg = R.drawable.bg_notification_price_button_unselected - - notificationLayout.setTextViewText(buttonId, - priceMap[buttonPriceId]?.toHumanReadableString()) - - if (buttonPriceId == selectedButtonId) { - notificationLayout.setInt(buttonId, "setBackgroundResource", selectedBg) - } else { - notificationLayout.setInt(buttonId, "setBackgroundResource", unselectedBg) - } - val selectPriceIntent = Intent(activity, - PaymentNotificationIntentService::class.java).apply { - action = ACTION_SELECT_PRICE + buttonPriceId - } - val flags = when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> PendingIntent.FLAG_UPDATE_CURRENT - else -> PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } - notificationLayout.setOnClickPendingIntent(buttonId, PendingIntent.getService( - activity, - 0, - selectPriceIntent, - flags)) - } - - val tinkoffPayIntent = getTinkoffPayIntent(activity, - requireNotNull(priceMap[selectedButtonId])) - notificationLayout.setOnClickPendingIntent(R.id.buttonPayOther, tinkoffPayIntent) - - val notification = createNotification(activity, tinkoffPayIntent, notificationLayout) - NotificationManagerCompat.from(activity).notify(NOTIFICATION_ID, notification) - } - - @RequiresApi(Build.VERSION_CODES.O) - fun createNotificationChannel(context: Context) { - val channelName = context.getString(R.string.notification_channel_name) - val notificationManager = context.getSystemService(NotificationManager::class.java) - val channel = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - channelName, - NotificationManager.IMPORTANCE_HIGH - ).apply { - enableLights(true) - enableVibration(true) - lightColor = Color.BLUE - } - - notificationManager.createNotificationChannel(channel) - } - - private fun getGooglePayIntent(activity: Activity, price: Money): PendingIntent { - val options = createNotificationPaymentOptions(activity, price) - val googleParams = GooglePayParams(TerminalsManager.selectedTerminalKey, - environment = SessionParams.GPAY_TEST_ENVIRONMENT) - - return SampleApplication.tinkoffAcquiring.createGooglePayPendingIntentForResult(activity, - googleParams, - options, NOTIFICATION_PAYMENT_REQUEST_CODE, NOTIFICATION_ID) - } - - private fun getTinkoffPayIntent(activity: Activity, price: Money): PendingIntent { - val options = createNotificationPaymentOptions(activity, price) - return SampleApplication.tinkoffAcquiring.createTinkoffPaymentPendingIntentForResult(activity, - options, NOTIFICATION_PAYMENT_REQUEST_CODE, NOTIFICATION_ID) - } - - private fun createNotificationPaymentOptions(activity: Activity, price: Money): PaymentOptions { - val settings = SettingsSdkManager(activity) - val sessionParams = TerminalsManager.selectedTerminal - - return PaymentOptions() - .setOptions { - orderOptions { - orderId = abs(Random().nextInt()).toString() - amount = price - title = activity.getString(R.string.notification_order_title_subscription_renewal) - } - customerOptions { - customerKey = sessionParams.customerKey - checkType = settings.checkType - } - featuresOptions { - localizationSource = AsdkSource(Language.RU) - useSecureKeyboard = settings.isCustomKeyboardEnabled - validateExpiryDate = settings.validateExpiryDate - cameraCardScanner = settings.cameraScanner - fpsEnabled = settings.isFpsEnabled - tinkoffPayEnabled = settings.isTinkoffPayEnabled - darkThemeMode = settings.resolveDarkThemeMode() - theme = settings.resolvePaymentStyle() - } - } - } - - private fun createNotification(context: Context, - intent: PendingIntent, - layout: RemoteViews): Notification { - return NotificationCompat.Builder(context, - NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.cart) - .setContentTitle(context.getString(R.string.notification_title)) - .setContentText(context.getString(R.string.notification_text)) - .setContentIntent(intent) - .setCustomBigContentView(layout) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(false) - .setOnlyAlertOnce(true) - .build() - } -} \ No newline at end of file diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SessionParams.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SessionParams.kt index 8aa1a70a..c209ac56 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SessionParams.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/SessionParams.kt @@ -51,7 +51,7 @@ data class SessionParams( "L+evz0+s60Qz5gbBRGfqCA57lUiB3hfXQZq5/q1YkABOHf9cR6Ov5nTRSOnjORgP\n" + "jwIDAQAB" - private const val DEFAULT_CUSTOMER_KEY = "TestSDK_CustomerKey1" + private const val DEFAULT_CUSTOMER_KEY = "TestSDK_CustomerKey1123413431" private const val DEFAULT_CUSTOMER_EMAIL = "user@example.com" val TEST_SDK = SessionParams("TestSDK", PASSWORD, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL) @@ -63,6 +63,8 @@ data class SessionParams( SessionParams("1578942570730", PASSWORD, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL, "SBP 3"), SessionParams("1661351612593", "45tnvz0kkyyz82mw", PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL, "With token"), SessionParams("1661161705205", null, PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL, "Without token"), + SessionParams("1584440932619", "dniplpm7ct3tg9e3", PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL, "Sbp pay with token"), + SessionParams("1674123391307", "rpcmn7osqle5sj2r", PUBLIC_KEY, DEFAULT_CUSTOMER_KEY, DEFAULT_CUSTOMER_EMAIL, "new merchant") ) } } 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 9d1c7fc2..92f0b5b0 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 @@ -21,10 +21,13 @@ import android.content.SharedPreferences import androidx.annotation.StyleRes import androidx.preference.PreferenceManager import ru.tinkoff.acquiring.sample.R +import ru.tinkoff.acquiring.sample.camera.DemoCameraScanActivity import ru.tinkoff.acquiring.sample.camera.DemoCameraScanner import ru.tinkoff.acquiring.sdk.cardscanners.CameraCardScanner +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.CardScannerContract import ru.tinkoff.acquiring.sdk.models.DarkThemeMode import ru.tinkoff.cardio.CameraCardIOScanner +import ru.tinkoff.cardio.CameraCardIOScannerContract /** * @author Mariya Chernyadieva @@ -53,6 +56,9 @@ class SettingsSdkManager(private val context: Context) { val yandexPayEnabled: Boolean get() = preferences.getBoolean(context.getString(R.string.acq_sp_yandex_pay), true) + val isEnableCombiInit: Boolean + get() = preferences.getBoolean(context.getString(R.string.acq_sp_combi_init), true) + val checkType: String get() { val defaultCheckType = context.getString(R.string.acq_sp_check_type_no) @@ -69,6 +75,13 @@ class SettingsSdkManager(private val context: Context) { } else DemoCameraScanner() } + val cameraScannerContract: CardScannerContract + get() { + val cardIOCameraScan = context.getString(R.string.acq_sp_camera_type_card_io) + val cameraScan = preferences.getString(context.getString(R.string.acq_sp_camera_type_id), cardIOCameraScan) + return if (cardIOCameraScan == cameraScan) CameraCardIOScannerContract else DemoCameraScanActivity.Contract + } + val handleCardListErrorInSdk: Boolean get() = preferences.getBoolean(context.getString(R.string.acq_sp_handle_cards_error), true) diff --git a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/TerminalsManager.kt b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/TerminalsManager.kt index 8100e513..eb183741 100644 --- a/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/TerminalsManager.kt +++ b/sample/src/main/java/ru/tinkoff/acquiring/sample/utils/TerminalsManager.kt @@ -25,7 +25,7 @@ object TerminalsManager { value.find { it.terminalKey == SessionParams.TEST_SDK.terminalKey } != null -> value else -> value.toMutableList().apply { add(0, SessionParams.TEST_SDK) } } - prefs.edit().putString(TERMINALS_KEY, gson.toJson(_terminals)).apply() + prefs.edit().putString(TERMINALS_KEY, gson.toJson(_terminals)).commit() if (terminals.find { it.terminalKey == _selectedTerminalKey } == null) { selectedTerminalKey = terminals.first().terminalKey @@ -37,7 +37,7 @@ object TerminalsManager { get() = _selectedTerminalKey!! set(value) { _selectedTerminalKey = value - prefs.edit().putString(SELECTED_TERMINAL_KEY, value).apply() + prefs.edit().putString(SELECTED_TERMINAL_KEY, value).commit() SampleApplication.initSdk(context, selectedTerminal) } @@ -65,7 +65,10 @@ object TerminalsManager { selectedTerminalKey = terminals.first().terminalKey } - fun findTerminal(terminalKey: String) = terminals.find { it.terminalKey == terminalKey } + fun findTerminal(terminalKey: String): SessionParams? { + val terminal = terminals.find { it.terminalKey == terminalKey } + return terminal + } fun requireTerminal(terminalKey: String) = findTerminal(terminalKey)!! } \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_card_logos.xml b/sample/src/main/res/layout/activity_card_logos.xml new file mode 100644 index 00000000..3bcd4a43 --- /dev/null +++ b/sample/src/main/res/layout/activity_card_logos.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_cart.xml b/sample/src/main/res/layout/activity_cart.xml index 3e54a0a2..92defdff 100644 --- a/sample/src/main/res/layout/activity_cart.xml +++ b/sample/src/main/res/layout/activity_cart.xml @@ -94,10 +94,34 @@ android:visibility="gone" tools:visibility="visible" /> + + + + - \ No newline at end of file + diff --git a/sample/src/main/res/layout/activity_details.xml b/sample/src/main/res/layout/activity_details.xml index 2fb423dc..f6e0cace 100644 --- a/sample/src/main/res/layout/activity_details.xml +++ b/sample/src/main/res/layout/activity_details.xml @@ -134,6 +134,20 @@ android:paddingHorizontal="8dp" android:adjustViewBounds="true"/> + + - \ No newline at end of file + diff --git a/sample/src/main/res/layout/dialog_asdk_env.xml b/sample/src/main/res/layout/dialog_asdk_env.xml new file mode 100644 index 00000000..13ef7537 --- /dev/null +++ b/sample/src/main/res/layout/dialog_asdk_env.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/menu/main_menu.xml b/sample/src/main/res/menu/main_menu.xml index 45f4ee05..17128f62 100644 --- a/sample/src/main/res/menu/main_menu.xml +++ b/sample/src/main/res/menu/main_menu.xml @@ -40,11 +40,6 @@ android:title="@string/activity_title_static_qr" app:showAsAction="never"/> - - - \ No newline at end of file + diff --git a/sample/src/main/res/values-ru/strings.xml b/sample/src/main/res/values-ru/strings.xml index 8bff6179..fead4cec 100644 --- a/sample/src/main/res/values-ru/strings.xml +++ b/sample/src/main/res/values-ru/strings.xml @@ -32,6 +32,7 @@ Сохраненные карты Отправить уведомление Подписка на книги + Окружение Автор Год выпуска @@ -50,6 +51,7 @@ Tinkoff Acquiring SDK v%1$s(%2$d) Оплатить через Оплатить по карте + Родительский платеж Итого к оплате: Невозможно совершить оплату @@ -74,6 +76,7 @@ Темная тема CheckType Модуль камеры + Combi - Init Невозможно завершить привязку карты Привязка карты была отменена @@ -83,4 +86,5 @@ Платежные уведомления Продление подписки Подписка успешно продлена! - \ No newline at end of file + Список банков + diff --git a/sample/src/main/res/values/preferences_keys.xml b/sample/src/main/res/values/preferences_keys.xml index 26de40a7..1bcbb12e 100644 --- a/sample/src/main/res/values/preferences_keys.xml +++ b/sample/src/main/res/values/preferences_keys.xml @@ -22,6 +22,7 @@ acq_sp_recurrent_payment acq_sp_android_pay acq_sp_yandex_pay + acq_sp_combi_init acq_sp_fps acq_sp_tinkoff_pay acq_sp_handle_cards_error diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 26e3b5b2..934731c0 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Saved cards Send notification Book Subscription + Environment Author Year @@ -49,6 +50,7 @@ Tinkoff Acquiring SDK v%1$s(%2$d) Pay with Pay with card + Init recurrent Total: Payment failed. Try later. @@ -73,6 +75,7 @@ Dark Mode CheckType Camera module + Combi - Init Attachment failed. Try later. Attachment canceled @@ -82,4 +85,5 @@ Payment notifications Subscription renewal Subscription renewed successfully! + Bank list diff --git a/sample/src/main/res/xml/settings.xml b/sample/src/main/res/xml/settings.xml index a0ea497d..64ffbc8a 100644 --- a/sample/src/main/res/xml/settings.xml +++ b/sample/src/main/res/xml/settings.xml @@ -57,6 +57,12 @@ android:title="@string/settings_title_yandex_pay" app:iconSpaceReserved="false" /> + + - \ No newline at end of file + diff --git a/ui/build.gradle b/ui/build.gradle index 17bf3b4a..fc24bfc8 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -27,6 +27,10 @@ android { lintOptions { disable 'VectorRaster', 'UseRequireDrawableLoadingForDrawables', 'ObsoleteSdkInt', 'UnusedResources', 'IconDipSize', 'IconLocation' } + + buildFeatures { + viewBinding true + } } tasks.dokkaHtmlPartial.configure { @@ -38,6 +42,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleExtensionsVersion" implementation "ru.tinkoff.core.components.nfc:nfc:$coreNfcVersion" + implementation "ru.tinkoff.decoro:decoro:$decoroVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" @@ -57,17 +62,22 @@ dependencies { // threeds dependencies implementation "androidx.appcompat:appcompat:${appCompatVersion}" + implementation "androidx.fragment:fragment-ktx:${fragmentKtxVersion}" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleRuntimeVersion}" implementation "androidx.constraintlayout:constraintlayout:${constraintLayoutVersion}" implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" implementation "com.nimbusds:nimbus-jose-jwt:${nimbusVersion}" implementation "org.bouncycastle:bcprov-jdk15on:${bouncyCastleVersion}" implementation "jp.wasabeef:blurry:${blurryVersion}" implementation "com.scottyab:rootbeer-lib:${rootBeerVersion}" + implementation "androidx.recyclerview:recyclerview:${recyclerviewVersion}" implementation "com.google.android.material:material:${materialVersion}" testImplementation 'junit:junit:4.13' - testImplementation "org.mockito.kotlin:mockito-kotlin:${mokitoKotlin}" - testImplementation 'org.mockito:mockito-inline:2.13.0' + testImplementation "org.mockito.kotlin:mockito-kotlin:${mokitoKotlinVersion}" + testImplementation "org.mockito:mockito-inline:${mokitoInlineVersion}" + testImplementation "app.cash.turbine:turbine:${turbineVersion}" + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' -} \ No newline at end of file +} diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index e897553c..69d54525 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -25,24 +25,27 @@ - + + + - + + + + - - + android:windowSoftInputMode="adjustPan" /> + + + android:theme="@style/AcquiringTheme.Base" /> + + + + + + + + + android:windowSoftInputMode="adjustResize" + android:theme="@style/AcquiringNewTheme" /> PendingIntent.FLAG_UPDATE_CURRENT - else -> PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } - - return PendingIntent.getActivity( - activity, - requestCode, - intent, - flags - ) - } - - /** - * Запуск экрана Acquiring SDK для проведения оплаты - * - * @param activity контекст для запуска экрана из Activity - * @param options настройки платежной сессии и визуального отображения экрана - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK - * @param state вспомогательный параметр для запуска экрана Acquiring SDK - * с заданного состояния - */ - @JvmOverloads - fun openPaymentScreen(activity: Activity, options: PaymentOptions, requestCode: Int, state: AsdkState = DefaultState) { - options.asdkState = state - val intent = prepareIntent(activity, options, PaymentActivity::class.java) - activity.startActivityForResult(intent, requestCode) - } - /** * Запуск экрана Acquiring SDK для проведения оплаты * @@ -185,73 +70,32 @@ class TinkoffAcquiring( paymentId: Long? = null ) { options.asdkState = YandexPayState(yandexPayToken, paymentId) - val intent = prepareIntent(activity, options, YandexPaymentActivity::class.java) + val intent = prepareIntent(activity, options, YandexPaymentActivity::class) activity.startActivityForResult(intent, requestCode) } /** - * Запуск экрана Acquiring SDK для проведения оплаты - * - * @param fragment контекст для запуска экрана из Fragment - * @param options настройки платежной сессии и визуального отображения экрана - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK - * @param state вспомогательный параметр для запуска экрана Acquiring SDK - * с заданного состояния + * Создает платежную сессию в рамках оплаты по Системе быстрых платежей */ - @JvmOverloads - fun openPaymentScreen(fragment: Fragment, options: PaymentOptions, requestCode: Int, state: AsdkState = DefaultState) { - options.asdkState = state - val intent = prepareIntent(fragment.requireContext(), options, PaymentActivity::class.java) - fragment.startActivityForResult(intent, requestCode) + @MainThread + fun initSbpPaymentSession() { + SbpPaymentProcess.init(sdk, applicationContext.packageManager) } /** - * Запуск SDK для оплаты через Систему быстрых платежей - * - * @param activity контекст для запуска экрана из Activity - * @param options настройки платежной сессии - * @param requestCode код для получения результата, по завершению работы SDK + * Создает платежную сессию в рамках оплаты по tinkoffPay */ - fun payWithSbp(activity: Activity, options: PaymentOptions, requestCode: Int) { - openPaymentScreen(activity, options, requestCode, FpsState) + @MainThread + fun initTinkoffPayPaymentSession() { + TpayProcess.init(sdk) } /** - * Запуск SDK для оплаты через Систему быстрых платежей - * - * @param fragment контекст для запуска экрана из Fragment - * @param options настройки платежной сессии - * @param requestCode код для получения результата, по завершению работы SDK + * Создает платежную сессию в рамках оплаты по MirPay */ - fun payWithSbp(fragment: Fragment, options: PaymentOptions, requestCode: Int) { - openPaymentScreen(fragment, options, requestCode, FpsState) - } - - /** - * Запуск SDK для оплаты через Систему быстрых платежей - * - * @param paymentId уникальный идентификатор транзакции в системе банка, - * полученный после проведения инициации платежа - */ - fun payWithSbp(paymentId: Long): PaymentProcess { - return PaymentProcess(sdk, applicationContext).createInitializedSbpPaymentProcess(paymentId) - } - - /** - * Проверка статуса возможности оплата с помощью Tinkoff Pay - */ - fun checkTinkoffPayStatus( - onSuccess: (TinkoffPayStatusResponse) -> Unit, - onFailure: ((Throwable) -> Unit)? = null - ) { - CoroutineScope(Dispatchers.IO).launch { - val mainScope = this - val result = sdk.tinkoffPayStatus().performSuspendRequest() - withContext(Dispatchers.Main) { - result.fold(onSuccess = onSuccess, onFailure = { onFailure?.invoke(it) }) - mainScope.cancel() - } - } + @MainThread + fun initMirPayPaymentSession() { + MirPayProcess.init(sdk) } /** @@ -272,75 +116,6 @@ class TinkoffAcquiring( } } - /** - * Запуск SDK для оплаты через Tinkoff Pay. У возвращенгого объекта следует указать - * слушатель событий с помощью метода [PaymentProcess.subscribe] и вызвать метод - * [PaymentProcess.start] для запуска сценария оплаты. - * - * @param options настройки платежной сессии - * @param version версия Tinkoff Pay - */ - fun payWithTinkoffPay(options: PaymentOptions, version: String): PaymentProcess { - return PaymentProcess(sdk, applicationContext).createTinkoffPayPaymentProcess(options, version) - } - - - /** - * Запуск экрана Acquiring SDK для привязки новой карты - * - * @param activity контекст для запуска экрана из Activity - * @param options настройки привязки карты и визуального отображения экрана - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK - */ - fun openAttachCardScreen(activity: Activity, options: AttachCardOptions, requestCode: Int) { - val intent = prepareIntent(activity, options, AttachCardActivity::class.java) - activity.startActivityForResult(intent, requestCode) - } - - /** - * Запуск экрана Acquiring SDK для привязки новой карты - * - * @param fragment контекст для запуска экрана из Fragment - * @param options настройки привязки карты и визуального отображения экрана - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK - */ - fun openAttachCardScreen(fragment: Fragment, options: AttachCardOptions, requestCode: Int) { - val intent = prepareIntent(fragment.requireContext(), options, AttachCardActivity::class.java) - fragment.startActivityForResult(intent, requestCode) - } - - /** - * Запуск экрана Acquiring SDK для просмотра сохраненных карт - * - * @param activity контекст для запуска экрана из Activity - * @param savedCardsOptions настройки экрана сохраненных карт - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK. - * В случае удаления/добавления карты на экране, возвращается intent с - * параметром Boolean по ключу [TinkoffAcquiring.EXTRA_CARD_LIST_CHANGED] - * В случае выбора покупателем приоритетной карты, возвращается intent - * с параметром String по ключу [TinkoffAcquiring.EXTRA_CARD_ID] - */ - fun openSavedCardsScreen(activity: Activity, savedCardsOptions: SavedCardsOptions, requestCode: Int) { - val intent = prepareIntent(activity, savedCardsOptions, SavedCardsActivity::class.java) - activity.startActivityForResult(intent, requestCode) - } - - /** - * Запуск экрана Acquiring SDK для просмотра сохраненных карт - * - * @param fragment контекст для запуска экрана из Fragment - * @param savedCardsOptions настройки экрана сохраненных карт - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK. - * В случае удаления/добавления карты на экране, возвращается intent с - * параметром Boolean по ключу [TinkoffAcquiring.EXTRA_CARD_LIST_CHANGED] - * В случае выбора покупателем приоритетной карты, возвращается intent - * с параметром String по ключу [TinkoffAcquiring.EXTRA_CARD_ID] - */ - fun openSavedCardsScreen(fragment: Fragment, savedCardsOptions: SavedCardsOptions, requestCode: Int) { - val intent = prepareIntent(fragment.requireContext(), savedCardsOptions, SavedCardsActivity::class.java) - fragment.startActivityForResult(intent, requestCode) - } - /** * Запуск экрана с отображением QR кода для оплаты покупателем * @@ -349,7 +124,7 @@ class TinkoffAcquiring( * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK */ fun openDynamicQrScreen(activity: Activity, options: PaymentOptions, requestCode: Int) { - val intent = prepareIntent(activity, options, QrCodeActivity::class.java) + val intent = prepareIntent(activity, options, QrCodeActivity::class) activity.startActivityForResult(intent, requestCode) } @@ -361,7 +136,7 @@ class TinkoffAcquiring( * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK */ fun openDynamicQrScreen(fragment: Fragment, options: PaymentOptions, requestCode: Int) { - val intent = prepareIntent(fragment.requireContext(), options, QrCodeActivity::class.java) + val intent = prepareIntent(fragment.requireContext(), options, QrCodeActivity::class) fragment.startActivityForResult(intent, requestCode) } @@ -376,7 +151,7 @@ class TinkoffAcquiring( val options = BaseAcquiringOptions().apply { features = featuresOptions } - val intent = prepareIntent(activity, options, QrCodeActivity::class.java) + val intent = prepareIntent(activity, options, QrCodeActivity::class) activity.startActivityForResult(intent, requestCode) } @@ -391,132 +166,22 @@ class TinkoffAcquiring( val options = BaseAcquiringOptions().apply { features = featuresOptions } - val intent = prepareIntent(fragment.requireContext(), options, QrCodeActivity::class.java) + val intent = prepareIntent(fragment.requireContext(), options, QrCodeActivity::class) fragment.startActivityForResult(intent, requestCode) } - /** - * Запуск экрана с отображением QR кода для оплаты покупателем - * - * @param activity контекст для запуска экрана - * @param localization локализация экрана - * @param requestCode код для получения результата, по завершению работы экрана Acquiring SDK - */ - @Deprecated("Replaced with expanded method", - ReplaceWith("openStaticQrScreen(activity, FeaturesOptions().apply { localizationSource = localization }, requestCode)")) - fun openStaticQrScreen(activity: Activity, localization: LocalizationSource, requestCode: Int) { - openStaticQrScreen(activity, FeaturesOptions().apply { localizationSource = localization }, requestCode) - } - - /** - * Создает PendingIntent для вызова оплаты через GooglePay из уведомления. - * Результат оплаты будет обработан в SDK - * - * @param context контекст для запуска экрана - * @param googlePayParams параметры GooglePay - * @param options настройки платежной сессии - * @param notificationId ID уведомления. - * Если передан, уведомлене удалится в случае успешной оплаты - * @return настроенный PendingIntent - */ - @JvmOverloads - @Deprecated("Not supported yet") - fun createGooglePayPendingIntent(context: Context, - googlePayParams: GooglePayParams, - options: PaymentOptions, - notificationId: Int? = null): PendingIntent { - options.setTerminalParams(terminalKey, publicKey) - return NotificationPaymentActivity.createPendingIntent(context, - options, - null, - NotificationPaymentActivity.PaymentMethod.GPAY, - notificationId, - googlePayParams) - } - - /** - * Создает PendingIntent для вызова оплаты через экран оплаты Tinkoff из уведомления. - * Результат оплаты будет обработан в SDK - * - * @param context контекст для запуска экрана - * @param options настройки платежной сессии - * @param notificationId ID уведомления. - * Если передан, уведомлене удалится в случае успешной оплаты - * @return настроенный PendingIntent - */ - @JvmOverloads - fun createTinkoffPaymentPendingIntent(context: Context, options: PaymentOptions, notificationId: Int? = null): PendingIntent { - options.setTerminalParams(terminalKey, publicKey) - return NotificationPaymentActivity.createPendingIntent(context, - options, - null, - NotificationPaymentActivity.PaymentMethod.TINKOFF, - notificationId) - } - - /** - * Создает PendingIntent для вызова оплаты через GooglePay из уведомления. - * Результат вернется в onActivityResult с кодом [requestCode] (успех, ошибка или отмена) - * - * @param activity контекст для запуска экрана - * @param googlePayParams параметры GooglePay - * @param options настройки платежной сессии - * @param requestCode код для получения результата, по завершению оплаты - * @param notificationId ID уведомления. - * Если передан, уведомлене удалится в случае успешной оплаты - * @return настроенный PendingIntent - */ - @JvmOverloads - fun createGooglePayPendingIntentForResult(activity: Activity, - googlePayParams: GooglePayParams, - options: PaymentOptions, - requestCode: Int, - notificationId: Int? = null): PendingIntent { + private fun prepareIntent(context: Context, options: BaseAcquiringOptions, cls: KClass<*>): Intent { options.setTerminalParams(terminalKey, publicKey) - return NotificationPaymentActivity.createPendingIntent(activity, - options, - requestCode, - NotificationPaymentActivity.PaymentMethod.GPAY, - notificationId, - googlePayParams) + return BaseAcquiringActivity.createIntent(context, options, cls) } - /** - * Создает PendingIntent для вызова оплаты через экран оплаты Tinkoff из уведомления - * - * @param activity контекст для запуска экрана - * @param options настройки платежной сессии - * @param requestCode код для получения результата, по завершению оплаты - * @param notificationId ID уведомления. - * Если передан, уведомлене удалится в случае успешной оплаты - * @return настроенный PendingIntent - */ - @JvmOverloads - fun createTinkoffPaymentPendingIntentForResult(activity: Activity, - options: PaymentOptions, - requestCode: Int, - notificationId: Int? = null): PendingIntent { + fun attachCardOptions(setup: AttachCardOptions.() -> Unit) = AttachCardOptions().also { options -> options.setTerminalParams(terminalKey, publicKey) - return NotificationPaymentActivity.createPendingIntent(activity, - options, - requestCode, - NotificationPaymentActivity.PaymentMethod.TINKOFF, - notificationId) + setup(options) } - private fun prepareIntent(context: Context, options: BaseAcquiringOptions, cls: Class<*>): Intent { + fun savedCardsOptions(setup: SavedCardsOptions.() -> Unit) = SavedCardsOptions().also { options -> options.setTerminalParams(terminalKey, publicKey) - return BaseAcquiringActivity.createIntent(context, options, cls) - } - - companion object { - - const val RESULT_ERROR = 500 - const val EXTRA_ERROR = "extra_error" - const val EXTRA_CARD_ID = "extra_card_id" - const val EXTRA_PAYMENT_ID = "extra_payment_id" - const val EXTRA_REBILL_ID = "extra_rebill_id" - - const val EXTRA_CARD_LIST_CHANGED = "extra_cards_changed" + setup(options) } -} \ No newline at end of file +} diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/adapters/CardListAdapter.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/adapters/CardListAdapter.kt deleted file mode 100644 index eb1fe930..00000000 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/adapters/CardListAdapter.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.adapters - -import android.content.Context -import android.content.res.Configuration -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.ImageView -import android.widget.TextView -import ru.tinkoff.acquiring.sdk.R -import ru.tinkoff.acquiring.sdk.localization.AsdkLocalization -import ru.tinkoff.acquiring.sdk.models.Card -import ru.tinkoff.acquiring.sdk.ui.customview.Shadow -import ru.tinkoff.acquiring.sdk.utils.CardSystemIconsHolder - - -/** - * @author Mariya Chernyadieva - */ -internal class CardListAdapter(private val context: Context): BaseAdapter() { - - private var selectedCardId: String? = null - var moreClickListener: OnMoreIconClickListener? = null - var cardSelectListener: CardSelectListener? = null - - private var cards = mutableListOf() - private val iconsHolder: CardSystemIconsHolder = CardSystemIconsHolder(context) - private var isDarkMode = false - - init { - isDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { - var view: View? = convertView - val cardSelected = cards[position].cardId == selectedCardId - - if (view == null) { - view = LayoutInflater.from(context).inflate(R.layout.acq_item_card_list, parent, false) - } - view!!.findViewById(R.id.acq_item_card_background).background = if (cardSelected) { - Shadow(context, isDarkMode, R.color.acq_colorSelectedCardBackground).apply { shadowRadius = 0f } - } else { - Shadow(context, isDarkMode, R.color.acq_colorCardBackground) - } - - val cardImage = view.findViewById(R.id.acq_item_card_logo) - val cardNumber = view.findViewById(R.id.acq_item_card_number) - val cardDate = view.findViewById(R.id.acq_item_card_date) - - cardImage.setImageBitmap(iconsHolder.getCardSystemLogo(cards[position].pan!!)) - cardNumber.text = makeTextNumber(cards[position].pan!!) - cardDate.text = makeCardDate(cards[position].expDate!!) - val iconMore = view.findViewById(R.id.acq_item_card_more) - - iconMore.setOnClickListener { - moreClickListener?.onMoreIconClick(cards[position]) - } - - cardSelectListener?.let { listener -> - view.setOnClickListener { - selectedCardId = cards[position].cardId - listener.onCardSelected(cards[position]) - notifyDataSetChanged() - } - } - - return view - } - - override fun getItem(position: Int): Any { - return cards[position] - } - - override fun getItemId(position: Int): Long { - return 0 - } - - override fun getCount(): Int { - return cards.size - } - - fun setCards(cards: List) { - this.cards = cards.toMutableList() - notifyDataSetChanged() - } - - fun setSelectedCard(cardId: String) { - this.selectedCardId = cardId - notifyDataSetChanged() - } - - fun getLastPanNumbers(number: String): String { - return number.substring(number.length - 4, number.length) - } - - private fun makeTextNumber(number: String): String { - return String.format(AsdkLocalization.resources.cardListCardFormat!!, getLastPanNumbers(number)) - } - - private fun makeCardDate(date: String): String { - return "${date.subSequence(0, 2)}/${date.subSequence(2, date.length)}" - } - - interface OnMoreIconClickListener { - - fun onMoreIconClick(card: Card) - } - - interface CardSelectListener { - - fun onCardSelected(card: Card) - } -} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CameraCardScanner.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CameraCardScanner.kt index 1fe73d9c..39370182 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CameraCardScanner.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CameraCardScanner.kt @@ -31,6 +31,7 @@ interface CameraCardScanner : Serializable { /** * Запуск экрана сканирования карты */ + @Deprecated("use CardScannerWrapper and activity result api") fun startActivityForScanning(context: Context, requestCode: Int) /** @@ -47,4 +48,4 @@ interface CameraCardScanner : Serializable { const val REQUEST_CAMERA_CARD_SCAN = 4123 } -} \ No newline at end of file +} diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CardScanner.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CardScanner.kt index da41e9e7..941962f4 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CardScanner.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/CardScanner.kt @@ -63,7 +63,8 @@ internal class CardScanner(private val context: Context) { private fun openScanTypeDialog() { val localization = AsdkLocalization.resources - val itemsArray = arrayOf(localization.payDialogCardScanCamera, localization.payDialogCardScanNfc) + val itemsArray = + arrayOf(localization.payDialogCardScanCamera, localization.payDialogCardScanNfc) AlertDialog.Builder(context).apply { setItems(itemsArray) { dialog, item -> when (item) { @@ -75,9 +76,8 @@ internal class CardScanner(private val context: Context) { }.show() } - private fun startNfcScan() { - val cardFromNfcIntent = Intent(context, AsdkNfcScanActivity::class.java) - (context as Activity).startActivityForResult(cardFromNfcIntent, REQUEST_CARD_NFC) + private fun startNfcScan(): Intent { + return Intent(context, AsdkNfcScanActivity::class.java) } private fun startCameraScan() { diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerNewApi.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerNewApi.kt new file mode 100644 index 00000000..959acb69 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerNewApi.kt @@ -0,0 +1,117 @@ +package ru.tinkoff.acquiring.sdk.cardscanners.delegate + +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import ru.tinkoff.acquiring.sdk.cardscanners.models.ScannedCardData + +/** + * Created by i.golovachev + */ + +/** + * Интерфейс для инкапсуляции логики работы какого - либо метода сканирования + */ +interface CardScannerDelegate { + + /** + * проверка доступности метода сканирования + */ + val isEnabled: Boolean + + /** + * запуск процесса сканирования + */ + fun start() +} + +fun CardScannerDelegate?.isEnabled() = this?.isEnabled ?: false + +/** + * Интерфейс для инкапсуляции логики работы какого - либо метода сканирования + */ +typealias AsdkCardScanResultCallback = ActivityResultCallback + + +/** + * Множество , описывающее результат сканирования + */ +sealed class ScannedCardResult { + class Success(val data: ScannedCardData) : ScannedCardResult() + object Cancel : ScannedCardResult() + class Failure(val throwable: Throwable?) : ScannedCardResult() +} + +/** + * Контракт, для запуска экрана сканирования карты, и считывания результата + */ +abstract class CardScannerContract : + ActivityResultContract(), + java.io.Serializable + +/** + * Базовый класс, объеденяющий запуск экрана сканирования и обработку результата, + * основанный на new result api.Позволяет разделить обьявления и обработку результата в разных + * местах кода. + * + * activity - Требует activity для регистрации ActivityResultLauncher + * contract - Контракт открытия экрана и получения результата CardScannerContract + * callback - Обратный вызов, для использования полученных данных + * isEnabledChecker - метод, для проверки доступности метода + * scanKey - ключ, для регистрации коллбека для получения результата , используется NFC или Camera + * наследует CardScannerDelegate. + */ +open class AsdkCardScannerDelegate( + private val activityResultRegistry: ActivityResultRegistry, + private val activityLifecycle: Lifecycle, + private val activityScanCardContract: CardScannerContract, + private val scannedDataCallback: AsdkCardScanResultCallback, + private val scanType: CardScannerTypes, + private val isEnabledChecker: () -> Boolean, +) : CardScannerDelegate { + + constructor( + activity: ComponentActivity, + contract: CardScannerContract, + scanned: AsdkCardScanResultCallback, + scanType: CardScannerTypes, + isEnabledChecker: () -> Boolean, + ) : this( + activity.activityResultRegistry, + activity.lifecycle, + contract, + scanned, + scanType, + isEnabledChecker + ) + + private lateinit var launcher: ActivityResultLauncher + + init { + activityLifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + launcher = activityResultRegistry.register( + scanType.name, + activityScanCardContract, + scannedDataCallback + ) + } + }) + } + + override val isEnabled: Boolean + get() = isEnabledChecker() + + override fun start() { + launcher.launch(Unit) + } + + companion object { + const val SCAN_KEY = "extra_scan_key" + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerTypes.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerTypes.kt new file mode 100644 index 00000000..aeb7bb82 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerTypes.kt @@ -0,0 +1,8 @@ +package ru.tinkoff.acquiring.sdk.cardscanners.delegate + +/** + * Created by i.golovachev + */ +enum class CardScannerTypes { + NFC, CAMERA +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerWrapper.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerWrapper.kt new file mode 100644 index 00000000..52aae968 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/CardScannerWrapper.kt @@ -0,0 +1,76 @@ +package ru.tinkoff.acquiring.sdk.cardscanners.delegate + +import android.app.AlertDialog +import android.content.pm.PackageManager +import androidx.activity.ComponentActivity +import ru.tinkoff.acquiring.sdk.R +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.nfc.NfcCardScannerDelegate + +/** + * Created by i.golovachev + * + * Обертка , содержащая 2 спобоска сканирования: + * Nfc - реализован по дефолту, нет возможности поменять + * Camera - по дефолту недоступен, есть возможность реализовать на стороне приложения. + */ +internal class CardScannerWrapper( + private val activity: ComponentActivity, + private val callback: AsdkCardScanResultCallback +) : CardScannerDelegate { + + /** + * Метод, для установки кастомного контракта сканирования по камере + */ + var cameraCardScannerContract: CardScannerContract? = null + set(value) { + field = value + cameraCardScanner = if (field == null) { + null + } else { + AsdkCardScannerDelegate( + activity, field!!, callback, scanType = CardScannerTypes.CAMERA + ) { activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) && field != null } + } + } + + private var nfcCardScanner: AsdkCardScannerDelegate = NfcCardScannerDelegate(activity, callback) + private var cameraCardScanner: CardScannerDelegate? = null + + override fun start() { + when { + isNfcEnable() && isCameraEnable() -> openScanTypeDialog() + isNfcEnable() -> nfcCardScanner.start() + isCameraEnable() -> cameraCardScanner?.start() + } + } + + override val isEnabled: Boolean + get() = isCameraEnable() || isNfcEnable() + + private fun isCameraEnable() = cameraCardScanner.isEnabled() + + private fun isNfcEnable() = nfcCardScanner.isEnabled + + private fun openScanTypeDialog() { + val itemsArray = arrayOf(R.string.acq_scan_by_camera, R.string.acq_scan_by_nfc) + .map { activity.getString(it) }.toTypedArray() + + AlertDialog.Builder(activity).apply { + setItems(itemsArray) { dialog, item -> + when (item) { + CAMERA_ITEM -> cameraCardScanner?.start() + NFC_ITEM -> nfcCardScanner.start() + else -> throw IllegalStateException("unknown item for: $item") + } + dialog.dismiss() + } + }.show() + } + + companion object { + + private const val CAMERA_ITEM = 0 + private const val NFC_ITEM = 1 + } + +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/nfc/NfcCardScannerDelegate.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/nfc/NfcCardScannerDelegate.kt new file mode 100644 index 00000000..6f3d3f48 --- /dev/null +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/delegate/nfc/NfcCardScannerDelegate.kt @@ -0,0 +1,39 @@ +package ru.tinkoff.acquiring.sdk.cardscanners.delegate.nfc + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.activity.ComponentActivity +import ru.tinkoff.acquiring.sdk.cardscanners.delegate.* +import ru.tinkoff.acquiring.sdk.cardscanners.models.ScannedCardData +import ru.tinkoff.acquiring.sdk.cardscanners.ui.AsdkNfcScanActivity +import ru.tinkoff.acquiring.sdk.cardscanners.ui.AsdkNfcScanActivity.Companion.EXTRA_CARD + +/** + * Дефолтная реализация сканированния карты по НФС + */ +internal class NfcCardScannerDelegate( + activity: ComponentActivity, + callback: AsdkCardScanResultCallback +) : AsdkCardScannerDelegate( + activity = activity, + contract = NfcCardScannerContract, + callback, + CardScannerTypes.NFC, + isEnabledChecker = { activity.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC) } +) + +object NfcCardScannerContract : CardScannerContract() { + override fun createIntent(context: Context, input: Unit) = + Intent(context, AsdkNfcScanActivity::class.java) + + override fun parseResult(resultCode: Int, intent: Intent?): ScannedCardResult { + return when (resultCode) { + Activity.RESULT_OK -> ScannedCardResult.Success(intent!!.getSerializableExtra(EXTRA_CARD) as ScannedCardData) + Activity.RESULT_CANCELED -> ScannedCardResult.Cancel + AsdkNfcScanActivity.RESULT_ERROR -> ScannedCardResult.Failure(null) + else -> throw java.lang.IllegalStateException("unknown code: $resultCode") + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/ui/AsdkNfcScanActivity.kt b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/ui/AsdkNfcScanActivity.kt index 6814ef81..68b2cdd3 100644 --- a/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/ui/AsdkNfcScanActivity.kt +++ b/ui/src/main/java/ru/tinkoff/acquiring/sdk/cardscanners/ui/AsdkNfcScanActivity.kt @@ -58,14 +58,13 @@ internal class AsdkNfcScanActivity : AppCompatActivity() { override fun onNfcDisabled() = showDialog() }) - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN setupTranslucentStatusBar() val nfsDescription = findViewById(R.id.acq_nfc_tv_description) - nfsDescription.text = AsdkLocalization.resources.nfcDescription + nfsDescription.setText(R.string.acq_scan_by_nfc_description) val closeBtn = findViewById