From 780241736cca3488acb16d4b4b2da760b4fd9ebb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Oct 2025 12:53:49 +0400 Subject: [PATCH 1/9] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 73c8739caa..675625e1a8 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -33,7 +33,7 @@ }, { "name": "TANGEM_PAY_ENABLED", - "version": "undefined" + "version": "5.30.0" }, { "name": "NEW_TOKEN_RECEIVE_ENABLED", From 3ffffa73ec4a65223ff92899ebbc24fb63c4478d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Oct 2025 13:40:23 +0400 Subject: [PATCH 2/9] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 097bcd4c84..074d840e4c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -267,8 +267,8 @@ dependencies { debugImplementation(projects.features.kyc.impl) internalImplementation(projects.features.kyc.impl) mockedImplementation(projects.features.kyc.impl) - releaseImplementation(projects.features.kyc.mock) - externalImplementation(projects.features.kyc.mock) + releaseImplementation(projects.features.kyc.impl) + externalImplementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) From 616386c3450a36f87c2f31cec330e34c4b6cd140 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Oct 2025 16:12:30 +0500 Subject: [PATCH 3/9] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 34 +++-- .../tangem/tap/di/TangemSdkManagerModule.kt | 3 + .../sdk/impl/DefaultTangemSdkManager.kt | 24 +++- .../domain/sdk/impl/MockTangemSdkManager.kt | 6 + ...mPayGenerateAddressAndSignChallengeTask.kt | 130 ++++++++++++++++++ .../visa/VisaCustomerWalletApproveTask.kt | 17 +-- .../datasource/local/visa/TangemPayStorage.kt | 6 +- .../DefaultTangemPayAuthDataSource.kt | 35 +---- .../repository/DefaultOnboardingRepository.kt | 7 +- .../repository/TangemPayRequestPerformer.kt | 76 +++++----- .../domain/card/common/visa/VisaUtilities.kt | 2 + .../visa/model/TangemPayInitialCredentials.kt | 3 + .../pay/datasource/TangemPayAuthDataSource.kt | 3 +- .../DefaultTangemPayOnboardingComponent.kt | 7 +- .../model/TangemPayOnboardingModel.kt | 24 +++- .../tangempay/ui/TandemPayOnboardingScreen.kt | 46 +++++-- .../ui/TangemPayOnboardingScreenState.kt | 6 +- .../com/tangem/sdk/api/TangemSdkManager.kt | 6 +- 18 files changed, 310 insertions(+), 125 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index ce88071554..e615d85d62 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -2,8 +2,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -11,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton -import kotlin.text.encodeToByteArray private const val DEFAULT_KEY = "tangem_pay_default_key" private const val ORDER_ID_KEY = "tangem_pay_order_id_key" @@ -19,6 +19,7 @@ private const val ORDER_ID_KEY = "tangem_pay_order_id_key" @Singleton internal class DefaultTangemPayStorage @Inject constructor( @ApplicationContext applicationContext: Context, + @NetworkMoshi moshi: Moshi, private val dispatcherProvider: CoroutineDispatcherProvider, ) : TangemPayStorage { @@ -29,14 +30,21 @@ internal class DefaultTangemPayStorage @Inject constructor( name = "tangem_pay_storage", ) } - private val moshi by lazy { - Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - } private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } + override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { + withContext(dispatcherProvider.io) { + secureStorage.store(key = createCustomerAddressKey(userWalletId), value = customerWalletAddress) + } + } + + override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? { + return withContext(dispatcherProvider.io) { + secureStorage.getAsString(createCustomerAddressKey(userWalletId)) + } + } + override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) @@ -71,10 +79,14 @@ internal class DefaultTangemPayStorage @Inject constructor( secureStorage.delete(createOrderIdKey(customerWalletAddress)) } - override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { - secureStorage.delete(createKey(customerWalletAddress)) - secureStorage.delete(createOrderIdKey(customerWalletAddress)) - } + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + withContext(dispatcherProvider.io) { + secureStorage.delete(createCustomerAddressKey(userWalletId)) + secureStorage.delete(createKey(customerWalletAddress)) + secureStorage.delete(createOrderIdKey(customerWalletAddress)) + } + + private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index d741fc1a41..0487d0fad7 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler import dagger.Module @@ -27,6 +28,7 @@ internal class TangemSdkManagerModule { cardSdkConfigRepository: CardSdkConfigRepository, visaCardScanHandler: VisaCardScanHandler, visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -37,6 +39,7 @@ internal class TangemSdkManagerModule { resources = context.resources, visaCardScanHandler = visaCardScanHandler, visaCardActivationTaskFactory = visaCardActivationTaskFactory, + tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 09a9039974..32f0e5b105 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -18,13 +18,17 @@ import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.sign +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -44,6 +48,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask @@ -62,6 +67,7 @@ internal class DefaultTangemSdkManager( private val resources: Resources, private val visaCardScanHandler: VisaCardScanHandler, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, + private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : TangemSdkManager { @@ -511,6 +517,18 @@ internal class DefaultTangemSdkManager( ) } + override suspend fun tangemPayProduceInitialCredentials( + cardId: String, + ): CompletionResult { + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), + cardId = cardId, + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + } + // endregion companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 1568e7dfc8..d86ebd543b 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -213,5 +213,11 @@ class MockTangemSdkManager( error("Not implemented") } + override suspend fun tangemPayProduceInitialCredentials( + cardId: String, + ): CompletionResult { + error("Not implemented") + } + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt new file mode 100644 index 0000000000..000ae515ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -0,0 +1,130 @@ +package com.tangem.tap.domain.tasks.visa + +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.core.error.ext.tangemError +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.sign +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( + @Assisted private val coroutineScope: CoroutineScope, + private val dispatchersProvider: CoroutineDispatcherProvider, + private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + coroutineScope.launch { + callback(runSuspend(session = session)) + } + } + + private suspend fun runSuspend(session: CardSession): CompletionResult { + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) + + val derivationResult = runDerivationTask(session, wallet) + val address = when (derivationResult) { + is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error) + is CompletionResult.Success -> generateAddressFromExtendedKey(derivationResult.data) + } + + val challenge = withContext(dispatchersProvider.io) { + visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address) + }.getOrElse { return CompletionResult.Failure(it.tangemError) } + + val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge) + val approveResult = runVisaCustomerWalletApproveTask( + session = session, + cardId = card.cardId, + targetAddress = address, + dataToSign = dataToSign, + ) + val signedData = when (approveResult) { + is CompletionResult.Failure<*> -> return CompletionResult.Failure(approveResult.error) + is CompletionResult.Success -> approveResult.data + } + + val authTokens = withContext(dispatchersProvider.io) { + visaAuthRemoteDataSource.getTokenWithCustomerWallet( + sessionId = challenge.session.sessionId, + signature = signedData.signature, + nonce = signedData.dataToSign.hashToSign, + ) + }.getOrNull() ?: return CompletionResult.Failure(VisaActivationError.FailedRemoteState.tangemError) + + return CompletionResult.Success( + data = TangemPayInitialCredentials( + customerWalletAddress = address, + authTokens = authTokens, + ), + ) + } + + private suspend fun runDerivationTask( + session: CardSession, + wallet: CardWallet, + ): CompletionResult { + val deferred = CompletableDeferred>() + val derivationTask = DeriveWalletPublicKeyTask( + walletPublicKey = wallet.publicKey, + derivationPath = VisaUtilities.customDerivationPath, + ) + + derivationTask.run(session = session, callback = deferred::complete) + return deferred.await() + } + + private suspend fun runVisaCustomerWalletApproveTask( + session: CardSession, + cardId: String, + targetAddress: String, + dataToSign: VisaDataToSignByCustomerWallet, + ): CompletionResult { + val deferred = CompletableDeferred>() + val task = VisaCustomerWalletApproveTask( + visaDataForApprove = VisaCustomerWalletApproveTask.Input( + cardId = cardId, + targetAddress = targetAddress, + hashToSign = dataToSign.hashToSign, + sign = dataToSign::sign, + ), + ) + task.run(session = session, callback = deferred::complete) + return deferred.await() + } + + private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { + val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey( + extendedPublicKey = extendedPublicKey, + cachedIndex = null, + ) + return derivationData.address + } + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 64fc98c01d..3439d72093 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -16,7 +16,6 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation @@ -59,21 +58,7 @@ class VisaCustomerWalletApproveTask( session: CardSession, callback: CompletionCallback, ) { - val cardDTO = CardDTO(card) - - val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run { - proceedApproveWithLegacyCard( - card = card, - session = session, - callback = callback, - ) - return - } - - val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run { - callback(CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError)) - return - } + val derivationPath = VisaUtilities.customDerivationPath val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index bf8bf7fa78..3d9558f45e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -1,9 +1,13 @@ package com.tangem.datasource.local.visa +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.visa.model.VisaAuthTokens interface TangemPayStorage { + suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) + suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? + suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? @@ -14,5 +18,5 @@ interface TangemPayStorage { suspend fun clearOrderId(customerWalletAddress: String) - suspend fun clearAll(customerWalletAddress: String) + suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 450fc7c70d..bca25d2bb9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -4,9 +4,8 @@ import arrow.core.Either import arrow.core.raise.either import com.tangem.common.CompletionResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.sdk.api.TangemSdkManager import javax.inject.Inject @@ -16,29 +15,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( private val tangemSdkManager: TangemSdkManager, ) : TangemPayAuthDataSource { - override suspend fun generateNewAuthTokens(address: String, cardId: String): Either = - either { - val challenge = visaAuthRemoteDataSource - .getCustomerWalletAuthChallenge(address) - .mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") } - .bind() + override suspend fun produceInitialCredentials(cardId: String): Either { + val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId) - val signed = tangemSdkManager.visaCustomerWalletApprove( - VisaDataForApprove( - customerWalletCardId = cardId, - targetAddress = address, - dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge), - ), - ).toEither { IllegalStateException("TangemPay signing failed: $it") }.bind() - - visaAuthRemoteDataSource.getTokenWithCustomerWallet( - sessionId = challenge.session.sessionId, - signature = signed.signature, - nonce = signed.dataToSign.hashToSign, - ) - .mapLeft { IllegalStateException("TangemPay token fetch failed. Error code: ${it.errorCode}") } - .bind() + return when (initialCredentials) { + is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error) + is CompletionResult.Success -> Either.Right(initialCredentials.data) } + } override suspend fun refreshAuthTokens(refreshToken: String): Either = either { visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens( @@ -47,9 +31,4 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .bind() } -} - -private fun CompletionResult.toEither(map: (Throwable) -> Throwable) = when (this) { - is CompletionResult.Success -> Either.Right(data) - is CompletionResult.Failure -> Either.Left(map(error)) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index dbe40d9c20..d662e386ce 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest @@ -30,9 +31,9 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun validateDeeplink(link: String): Either { return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request { - tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) - }.result + val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) + .getOrThrow() + .result result?.status == VALID_STATUS } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index c52ec65d24..e22793e4ed 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -2,9 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.Blockchain import com.tangem.core.error.UniversalError -import com.tangem.data.common.network.NetworkFactory import com.tangem.data.pay.util.TangemPayErrorConverter import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.datasource.api.common.response.ApiResponse @@ -14,10 +12,10 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.visa.model.VisaAuthTokens -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -30,11 +28,9 @@ internal class TangemPayRequestPerformer @Inject constructor( private val tangemPayStorage: TangemPayStorage, private val authDataSource: TangemPayAuthDataSource, private val tangemPayWalletsManager: TangemPayWalletsManager, - private val walletManagersFacade: WalletManagersFacade, - private val networkFactory: NetworkFactory, ) { - private var customerWalletAddress: String? = null + private val customerWalletAddress = MutableStateFlow(null) private val refreshTokensMutex = Mutex() private var refreshTokensJob: Deferred? = null @@ -81,7 +77,9 @@ internal class TangemPayRequestPerformer @Inject constructor( getTokens: (suspend () -> VisaAuthTokens), refreshTokens: (suspend () -> VisaAuthTokens)? = null, ): T = runCatching { - requestBlock("Bearer ${getTokens().accessToken}").getOrThrow() + val tokens = getTokens() + val header = tokens.getAuthHeader() + requestBlock(header).getOrThrow() }.getOrElse { error -> val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) { @@ -116,7 +114,18 @@ internal class TangemPayRequestPerformer @Inject constructor( return result } - suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address + suspend fun getCustomerWalletAddress(): String { + val existingAddress = customerWalletAddress.value + if (existingAddress != null) { + return existingAddress + } + val storedAddress = tangemPayStorage.getCustomerWalletAddress( + userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId, + ) ?: error("Can not find customer address") + + customerWalletAddress.value = storedAddress + return storedAddress + } private suspend fun getAccessTokens(): VisaAuthTokens { return getAccessTokensIfSaved() ?: fetchTokens() @@ -126,42 +135,33 @@ internal class TangemPayRequestPerformer @Inject constructor( return tangemPayStorage.getAuthTokens(getCustomerWalletAddress()) } - private suspend fun fetchAuthInputData(): AuthInputData { - val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() - - val network = networkFactory.create( - blockchain = Blockchain.Polygon, - extraDerivationPath = null, - derivationStyleProvider = wallet.derivationStyleProvider, - canHandleTokens = true, - ) ?: error("Cannot create network") - - val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network) - ?: error("Cannot get polygon address") - - customerWalletAddress = address - - return AuthInputData(address, wallet.cardId) - } - private suspend fun fetchTokens(): VisaAuthTokens { - val inputData = fetchAuthInputData() - val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId) - .getOrNull() ?: error("Cannot fetch tokens") - tangemPayStorage.storeAuthTokens(inputData.address, tokens) - return tokens + val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() + val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId) + .getOrThrowWithMessage("Can not produce initial data:") + tangemPayStorage.storeCustomerWalletAddress( + userWalletId = wallet.walletId, + customerWalletAddress = initialCredentials.customerWalletAddress, + ) + tangemPayStorage.storeAuthTokens( + customerWalletAddress = initialCredentials.customerWalletAddress, + tokens = initialCredentials.authTokens, + ) + return initialCredentials.authTokens } private suspend fun refreshAuthTokens(): VisaAuthTokens { val customerWalletAddress = getCustomerWalletAddress() val refreshToken = getAccessTokens().refreshToken.value - val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens") + val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrThrowWithMessage("Cannot refresh tokens:") tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens) return tokens } -} -internal data class AuthInputData( - val address: String, - val cardId: String, -) \ No newline at end of file + private fun Either.getOrThrowWithMessage(message: String): B { + return this.fold( + ifLeft = { error -> throw IllegalStateException("$message ${error.message}") }, + ifRight = { it }, + ) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt index 101ec1b9a4..fe41ac4f05 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt @@ -3,6 +3,7 @@ package com.tangem.domain.card.common.visa import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.card.FirmwareVersion +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.CardDTO private const val VISA_BATCH_START = "AE" @@ -16,6 +17,7 @@ object VisaUtilities { val visaDefaultDerivationPath get() = visaBlockchain.derivationPath(DerivationStyle.V3) + val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0") fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt new file mode 100644 index 0000000000..40a1132513 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayInitialCredentials.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.visa.model + +data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 05725f0183..69ec635f4a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -1,11 +1,12 @@ package com.tangem.domain.pay.datasource import arrow.core.Either +import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaAuthTokens interface TangemPayAuthDataSource { - suspend fun generateNewAuthTokens(address: String, cardId: String): Either + suspend fun produceInitialCredentials(cardId: String): Either suspend fun refreshAuthTokens(refreshToken: String): Either } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 01effd1f8b..59457418df 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -22,12 +22,7 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.screenState.collectAsStateWithLifecycle() - TandemPayOnboardingScreen( - modifier = modifier, - state = state, - onBackClick = model::back, - onOpenKycClick = model::openKyc, - ) + TandemPayOnboardingScreen(modifier = modifier, state = state) } @AssistedFactory diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 4b41785218..a3767562ec 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -12,6 +12,7 @@ import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,7 +28,7 @@ internal class TangemPayOnboardingModel @Inject constructor( private val params = paramsContainer.require() val screenState: StateFlow - field = MutableStateFlow(TangemPayOnboardingScreenState()) + field = MutableStateFlow(getInitialState()) init { modelScope.launch { @@ -37,21 +38,27 @@ internal class TangemPayOnboardingModel @Inject constructor( } is TangemPayOnboardingComponent.Params.Deeplink -> { repository.validateDeeplink(params.deeplink) - .onRight { isValid -> if (isValid) checkCustomerInfo() } + .onRight { isValid -> if (isValid) showOnboarding() } .onLeft { back() } } } } } - fun openKyc() { + private fun openKyc() { router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) } - fun back() { + private fun back() { router.pop() } + private fun showOnboarding() { + screenState.update { + it.copy(fullScreenLoading = false) + } + } + private suspend fun checkCustomerInfo() { repository.getCustomerInfo() .onRight { customerInfo -> @@ -68,4 +75,13 @@ internal class TangemPayOnboardingModel @Inject constructor( } .onLeft { back() } } + + private fun getInitialState(): TangemPayOnboardingScreenState { + return TangemPayOnboardingScreenState( + fullScreenLoading = true, + buttonLoading = false, + onGetCardClick = ::openKyc, + onBackClick = ::back, + ) + } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt index 8f2605df2e..54f4140025 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt @@ -9,24 +9,21 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero @Composable -internal fun TandemPayOnboardingScreen( - state: TangemPayOnboardingScreenState, - onOpenKycClick: () -> Unit, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) { Scaffold( modifier = modifier.systemBarsPadding(), topBar = { AppBarWithBackButton( modifier = Modifier.statusBarsPadding(), - onBackClick = onBackClick, + onBackClick = state.onBackClick, iconRes = R.drawable.ic_back_24, ) }, @@ -37,7 +34,7 @@ internal fun TandemPayOnboardingScreen( .padding(paddingValues) .fillMaxSize(), state = state, - onButtonClick = onOpenKycClick, + onButtonClick = state.onGetCardClick, ) }, ) @@ -46,8 +43,35 @@ internal fun TandemPayOnboardingScreen( @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewDarkTheme() { +private fun PreviewDarkTheme( + @PreviewParameter(TangemPayOnboardingScreenStateProvider::class) + state: TangemPayOnboardingScreenState, +) { TangemThemePreview { - TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}, {}) + TandemPayOnboardingScreen(state = state, modifier = Modifier.fillMaxSize()) } -} \ No newline at end of file +} + +private class TangemPayOnboardingScreenStateProvider : + CollectionPreviewParameterProvider( + listOf( + TangemPayOnboardingScreenState( + fullScreenLoading = true, + buttonLoading = false, + onGetCardClick = {}, + onBackClick = {}, + ), + TangemPayOnboardingScreenState( + fullScreenLoading = false, + buttonLoading = false, + onGetCardClick = {}, + onBackClick = {}, + ), + TangemPayOnboardingScreenState( + fullScreenLoading = false, + buttonLoading = true, + onGetCardClick = {}, + onBackClick = {}, + ), + ), + ) \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt index 8745cf3db9..9cf4deb4d6 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingScreenState.kt @@ -4,6 +4,8 @@ import javax.annotation.concurrent.Immutable @Immutable internal data class TangemPayOnboardingScreenState( - val fullScreenLoading: Boolean = true, - val buttonLoading: Boolean = false, + val fullScreenLoading: Boolean, + val buttonLoading: Boolean, + val onGetCardClick: () -> Unit, + val onBackClick: () -> Unit, ) \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 12aeb18278..25bf3febfa 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -16,7 +16,10 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse @@ -156,5 +159,6 @@ interface TangemSdkManager { visaDataForApprove: VisaDataForApprove, ): CompletionResult + suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult // endregion } \ No newline at end of file From b81ea5cf6ed2e55b51aa1d59b2810813cf1ad374 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Oct 2025 20:07:04 +0500 Subject: [PATCH 4/9] Updated on 2026-08-14 --- .../tangem/data/pay/di/TangemPayDataModule.kt | 9 +++ .../repository/DefaultOnboardingRepository.kt | 33 ++++++++++- .../repository/TangemPayRequestPerformer.kt | 45 +++------------ .../pay/repository/OnboardingRepository.kt | 4 ++ .../ProduceTangemPayInitialDataUseCase.kt | 21 +++++++ .../onboarding/impl/build.gradle.kts | 3 + .../DefaultTangemPayOnboardingComponent.kt | 2 +- .../model/TangemPayOnboardingModel.kt | 57 ++++++++++++++----- .../wallet/child/wallet/model/WalletModel.kt | 18 ++++-- 9 files changed, 133 insertions(+), 59 deletions(-) create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index c6222c62c7..40c402edf8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.pay.DataForReceiveFactory import com.tangem.domain.pay.repository.CardDetailsRepository import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository @@ -52,6 +53,14 @@ internal interface TangemPayDataModule { return TangemPayMainScreenCustomerInfoUseCase(repository = repository) } + @Provides + @Singleton + fun provideProduceTangemPayInitialDataUseCase( + repository: OnboardingRepository, + ): ProduceTangemPayInitialDataUseCase { + return ProduceTangemPayInitialDataUseCase(repository = repository) + } + @Provides @Singleton fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index d662e386ce..2f5582be20 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -2,12 +2,14 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.OrderRequest import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.ProductInstance @@ -27,6 +29,8 @@ internal class DefaultOnboardingRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, private val tangemPayStorage: TangemPayStorage, + private val authDataSource: TangemPayAuthDataSource, + private val tangemPayWalletsManager: TangemPayWalletsManager, ) : OnboardingRepository { override suspend fun validateDeeplink(link: String): Either { @@ -38,6 +42,31 @@ internal class DefaultOnboardingRepository @Inject constructor( } } + override suspend fun isTangemPayInitialDataProduced(): Boolean { + val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId + val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false + tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false + + return true + } + + override suspend fun produceInitialData() { + val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() + val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId) + .fold( + ifLeft = { error -> error("Can not produce initial data: ${error.message}") }, + ifRight = { it }, + ) + tangemPayStorage.storeCustomerWalletAddress( + userWalletId = wallet.walletId, + customerWalletAddress = initialCredentials.customerWalletAddress, + ) + tangemPayStorage.storeAuthTokens( + customerWalletAddress = initialCredentials.customerWalletAddress, + tokens = initialCredentials.authTokens, + ) + } + override suspend fun getCustomerInfo(): Either { return requestHelper.runWithErrorLogs(TAG) { val result = requestHelper.request { authHeader -> @@ -85,7 +114,7 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun createOrder(): Either = withContext(dispatcherProvider.io) { requestHelper.runWithErrorLogs(TAG) { val walletAddress = requestHelper.getCustomerWalletAddress() - val result = requestHelper.requestWithPersistedToken { authHeader -> + val result = requestHelper.request { authHeader -> tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) }.result ?: error("Create order result is null") @@ -129,7 +158,7 @@ internal class DefaultOnboardingRepository @Inject constructor( } private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo { - val result = requestHelper.requestWithPersistedToken { authHeader -> + val result = requestHelper.request { authHeader -> tangemPayApi.getCustomerMe(authHeader) }.result return getCustomerInfo(result) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index e22793e4ed..cd24329183 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -63,15 +63,6 @@ internal class TangemPayRequestPerformer @Inject constructor( ) } - suspend fun requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse): T = - withContext(dispatchers.io) { - performRequest( - requestBlock = requestBlock, - getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") }, - refreshTokens = ::refreshAuthTokens, - ) - } - private suspend fun performRequest( requestBlock: suspend (header: String) -> ApiResponse, getTokens: (suspend () -> VisaAuthTokens), @@ -128,40 +119,20 @@ internal class TangemPayRequestPerformer @Inject constructor( } private suspend fun getAccessTokens(): VisaAuthTokens { - return getAccessTokensIfSaved() ?: fetchTokens() - } - - private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? { - return tangemPayStorage.getAuthTokens(getCustomerWalletAddress()) - } - - private suspend fun fetchTokens(): VisaAuthTokens { - val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay() - val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId) - .getOrThrowWithMessage("Can not produce initial data:") - tangemPayStorage.storeCustomerWalletAddress( - userWalletId = wallet.walletId, - customerWalletAddress = initialCredentials.customerWalletAddress, - ) - tangemPayStorage.storeAuthTokens( - customerWalletAddress = initialCredentials.customerWalletAddress, - tokens = initialCredentials.authTokens, - ) - return initialCredentials.authTokens + val walletAddress = getCustomerWalletAddress() + val tokens = tangemPayStorage.getAuthTokens(walletAddress) ?: error("Auth tokens are not stored") + return tokens } private suspend fun refreshAuthTokens(): VisaAuthTokens { val customerWalletAddress = getCustomerWalletAddress() val refreshToken = getAccessTokens().refreshToken.value - val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrThrowWithMessage("Cannot refresh tokens:") + val tokens = authDataSource.refreshAuthTokens(refreshToken) + .fold( + ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") }, + ifRight = { it }, + ) tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens) return tokens } - - private fun Either.getOrThrowWithMessage(message: String): B { - return this.fold( - ifLeft = { error -> throw IllegalStateException("$message ${error.message}") }, - ifRight = { it }, - ) - } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 5c93a97e19..88d98ab106 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -9,6 +9,10 @@ interface OnboardingRepository { suspend fun validateDeeplink(link: String): Either + suspend fun isTangemPayInitialDataProduced(): Boolean + + suspend fun produceInitialData() + suspend fun getCustomerInfo(): Either suspend fun createOrder(): Either diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt new file mode 100644 index 0000000000..b0f5cb8e91 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.pay.repository.OnboardingRepository + +class ProduceTangemPayInitialDataUseCase( + private val repository: OnboardingRepository, +) { + + suspend operator fun invoke(): Either { + return either { + val isDataProduced = repository.isTangemPayInitialDataProduced() + if (isDataProduced) { + return@either Unit + } else { + repository.produceInitialData() + } + } + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 0b2af70dd3..a118c239b7 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -39,4 +39,7 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.timber) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 59457418df..4ee5eb136c 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -21,7 +21,7 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state by model.screenState.collectAsStateWithLifecycle() + val state by model.uiState.collectAsStateWithLifecycle() TandemPayOnboardingScreen(modifier = modifier, state = state) } diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index a3767562ec..aedb40e989 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -23,11 +25,11 @@ internal class TangemPayOnboardingModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val repository: OnboardingRepository, + private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, ) : Model() { private val params = paramsContainer.require() - - val screenState: StateFlow + val uiState: StateFlow field = MutableStateFlow(getInitialState()) init { @@ -45,18 +47,8 @@ internal class TangemPayOnboardingModel @Inject constructor( } } - private fun openKyc() { - router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) - } - - private fun back() { - router.pop() - } - private fun showOnboarding() { - screenState.update { - it.copy(fullScreenLoading = false) - } + uiState.update { it.copy(fullScreenLoading = false) } } private suspend fun checkCustomerInfo() { @@ -66,7 +58,7 @@ internal class TangemPayOnboardingModel @Inject constructor( !customerInfo.isKycApproved -> { when (params) { is TangemPayOnboardingComponent.Params.Deeplink -> - screenState.value = screenState.value.copy(fullScreenLoading = false) + uiState.value = uiState.value.copy(fullScreenLoading = false) else -> openKyc() } } @@ -76,11 +68,46 @@ internal class TangemPayOnboardingModel @Inject constructor( .onLeft { back() } } + private fun onGetCardClick() { + uiState.update { it.copy(buttonLoading = true) } + modelScope.launch { + val result = produceInitialDataUseCase() + if (result.isLeft()) { + Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") + uiState.update { it.copy(buttonLoading = false) } + return@launch + } + + repository.getCustomerInfo() + .fold( + ifLeft = { + Timber.e("Error getCustomerInfo: ${it.errorCode}") + uiState.update { it.copy(buttonLoading = false) } + }, + ifRight = { customerInfo -> + if (customerInfo.isKycApproved) { + back() + } else { + openKyc() + } + }, + ) + } + } + + private fun openKyc() { + router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) + } + + private fun back() { + router.pop() + } + private fun getInitialState(): TangemPayOnboardingScreenState { return TangemPayOnboardingScreenState( fullScreenLoading = true, buttonLoading = false, - onGetCardClick = ::openKyc, + onGetCardClick = ::onGetCardClick, onBackClick = ::back, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 3faf4f6d3a..00f34e6cdf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* @@ -93,6 +94,7 @@ internal class WalletModel @Inject constructor( private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, + private val tangemPayOnboardingRepository: OnboardingRepository, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -344,16 +346,24 @@ internal class WalletModel @Inject constructor( * and every minute while user stays on the main screen */ screenLifecycleProvider.isBackgroundState.onEach { inBackground -> + // fast exit + if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach + updateTangemPayJobHolder.cancel() - if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) { - modelScope.launch { + + modelScope.launch { + // fast exit + val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced() + if (!initialDataProduced) return@launch + + if (!inBackground) { refreshTangemPayInfo() while (isActive) { delay(TANGEM_PAY_UPDATE_INTERVAL) refreshTangemPayInfo() } - }.saveIn(updateTangemPayJobHolder) - } + } + }.saveIn(updateTangemPayJobHolder) }.launchIn(modelScope) } From 8beefdf5e946ca21567051a6d5056302fafc99e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Oct 2025 13:50:20 +0500 Subject: [PATCH 5/9] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 3 +- .../core/ui/extensions/ColorReference.kt | 33 +++ .../DefaultTangemPayTxHistoryRepository.kt | 7 +- .../utils/TangemPayTxHistoryItemConverter.kt | 11 +- .../feedback/models/FeedbackEmailType.kt | 6 + .../domain/feedback/FeedbackDataBuilder.kt | 5 + .../feedback/SendFeedbackEmailUseCase.kt | 1 + .../utils/EmailMessageBodyResolver.kt | 13 +- .../utils/EmailMessageTitleResolver.kt | 1 + .../feedback/utils/EmailSubjectResolver.kt | 4 +- domain/models/build.gradle.kts | 1 + .../serialization/CurrencySerializer.kt | 24 ++ .../serialization/JodaDateTimeSerializer.kt | 24 ++ .../visa/model/TangemPayTxHistoryItem.kt | 40 +-- .../components/TangemPayDetailsComponent.kt | 3 +- .../tangempay/details/impl/build.gradle.kts | 6 +- .../DefaultTangemPayDetailsComponent.kt | 40 +-- .../TangemPayErrorBottomSheetComponent.kt | 23 -- .../DefaultTangemPayTxHistoryComponent.kt | 3 +- .../TangemPayTxHistoryDetailsComponent.kt | 34 +++ .../tangempay/di/TangemPayModelModule.kt | 6 + .../entity/TangemPayDetailsNavigation.kt | 15 ++ .../entity/TangemPayTxHistoryDetailsUM.kt | 23 ++ .../tangempay/model/TangemPayDetailsModel.kt | 17 +- .../model/TangemPayDetailsNavigation.kt | 17 -- .../model/TangemPayTxHistoryDetailsModel.kt | 66 +++++ .../model/TangemPayTxHistoryModel.kt | 11 +- .../TangemPayTxHistoryDetailsConverter.kt | 202 +++++++++++++++ .../tangempay/ui/InternalComponents.kt | 46 ++++ .../tangempay/ui/TangemPayTxHistoryUi.kt | 51 +--- .../tangempay/ui/TangempayTxDetailsUi.kt | 241 ++++++++++++++++++ .../utils/TangemPayErrorMessageFactory.kt | 8 +- .../wallet/child/wallet/model/WalletModel.kt | 7 +- .../router/DefaultWalletRouter.kt | 4 +- .../presentation/router/InnerWalletRouter.kt | 2 +- 36 files changed, 859 insertions(+), 141 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/serialization/CurrencySerializer.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/serialization/JodaDateTimeSerializer.kt delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayErrorBottomSheetComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsNavigation.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/InternalComponents.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUi.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index e75766a69a..6b0ad87897 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -604,7 +604,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.TangemPayDetails -> { createComponentChild( context = context, - params = TangemPayDetailsComponent.Params(config = route.config), + params = TangemPayDetailsComponent.Params(userWalletId = route.userWalletId, config = route.config), componentFactory = tangemPayDetailsComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index ea0474e6e9..868989544f 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -383,8 +383,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class TangemPayDetails( + val userWalletId: UserWalletId, val config: TangemPayDetailsConfig, - ) : AppRoute(path = "/tangem_pay_details") + ) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}") @Serializable data class TangemPayOnboarding( diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt new file mode 100644 index 0000000000..8add413e59 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ColorReference.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +/** + * Utility class for keeping themed color reference from app theme. + * + * It necessary to use [Immutable] annotation for runtime stability. + * + * @property value color provider from theme + */ +@Immutable +data class ColorReference(val value: @Composable () -> Color) + +/** + * Creates a [ColorReference] using a themed color from the app theme with a lambda. + * + * @param value The color provider from theme. + * @return A [ColorReference] representing the themed color. + */ +fun themedColor(value: @Composable () -> Color): ColorReference { + return ColorReference(value) +} + +/** + * Resolves [ColorReference] to [Color] + */ +@Composable +fun ColorReference.resolveReference(): Color { + return value() +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index 7e197fea52..b7c1bac9d4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -1,8 +1,10 @@ package com.tangem.data.pay.repository +import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext @@ -26,8 +28,11 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( private val cacheRegistry: CacheRegistry, private val txHistoryItemsStore: TangemPayTxHistoryItemsStore, private val dispatchers: CoroutineDispatcherProvider, + @NetworkMoshi private val moshi: Moshi, ) : TangemPayTxHistoryRepository { + private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) } + override fun getTxHistoryBatchFlow( batchSize: Int, context: TangemPayTxHistoryListBatchingContext, @@ -84,7 +89,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( val result = requestPerformer.request { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.result - val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull() + val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) }.onLeft { error(it.toString()) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt index e8bead6d27..8c916070f1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -1,14 +1,19 @@ package com.tangem.data.visa.utils +import com.squareup.moshi.Moshi import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.utils.converter.Converter import timber.log.Timber import java.util.Currency -internal object TangemPayTxHistoryItemConverter : +internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : Converter { + private val spendAdapter = moshi.adapter(TangemPayTxHistoryResponse.Spend::class.java) + private val paymentAdapter = moshi.adapter(TangemPayTxHistoryResponse.Payment::class.java) + private val feeAdapter = moshi.adapter(TangemPayTxHistoryResponse.Fee::class.java) + override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem? { return value.spend?.let { convertSpend(id = value.id, spend = it) } ?: value.payment?.let { convertPayment(id = value.id, payment = it) } @@ -22,6 +27,7 @@ internal object TangemPayTxHistoryItemConverter : private fun convertSpend(id: String, spend: TangemPayTxHistoryResponse.Spend): TangemPayTxHistoryItem.Spend { return TangemPayTxHistoryItem.Spend( id = id, + jsonRepresentation = spendAdapter.toJson(spend), // If postedAt is null, it means transaction wasn't posted and was likely declined. Use authorizedAt date = spend.postedAt ?: spend.authorizedAt, amount = spend.amount, @@ -41,15 +47,18 @@ internal object TangemPayTxHistoryItemConverter : ): TangemPayTxHistoryItem.Payment { return TangemPayTxHistoryItem.Payment( id = id, + jsonRepresentation = paymentAdapter.toJson(payment), date = payment.postedAt, currency = Currency.getInstance(payment.currency), amount = payment.amount, + transactionHash = payment.transactionHash, ) } private fun convertFee(id: String, fee: TangemPayTxHistoryResponse.Fee): TangemPayTxHistoryItem.Fee { return TangemPayTxHistoryItem.Fee( id = id, + jsonRepresentation = feeAdapter.toJson(fee), date = fee.postedAt, currency = Currency.getInstance(fee.currency), amount = fee.amount, diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 5649d73a23..df2d3bd01e 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -1,5 +1,6 @@ package com.tangem.domain.feedback.models +import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.VisaTxDetails /** @@ -64,5 +65,10 @@ sealed interface FeedbackEmailType { val visaTxDetails: VisaTxDetails, override val walletMetaInfo: WalletMetaInfo, ) : Visa() + + data class DisputeV2( + val item: TangemPayTxHistoryItem, + override val walletMetaInfo: WalletMetaInfo, + ) : Visa() } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index e5f65f4c77..6431a2da48 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -2,6 +2,7 @@ package com.tangem.domain.feedback import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.utils.breakLine +import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.VisaTxDetails import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses @@ -9,6 +10,10 @@ internal class FeedbackDataBuilder { private val builder = StringBuilder() + fun addTangemPayTxInfo(item: TangemPayTxHistoryItem) { + builder.append(item.jsonRepresentation) + } + fun addVisaTxInfo(txDetails: VisaTxDetails) { builder.appendKeyValue("Type", txDetails.type) builder.appendKeyValue("Status", txDetails.status) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index a8d5177b41..0aff9662cf 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -64,6 +64,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.PreActivatedWallet, is FeedbackEmailType.CardAttestationFailed, is FeedbackEmailType.Visa.Dispute, + is FeedbackEmailType.Visa.DisputeV2, -> this is FeedbackEmailType.DirectUserRequest, is FeedbackEmailType.RateCanBeBetter, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 5a94a88857..cf9a7a8208 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -1,9 +1,10 @@ package com.tangem.domain.feedback.utils import com.tangem.domain.feedback.FeedbackDataBuilder -import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.VisaTxDetails /** @@ -33,11 +34,21 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails) + is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayRequestBody(type.walletMetaInfo, type.item) } return build() } + private suspend fun FeedbackDataBuilder.addTangemPayRequestBody( + walletMetaInfo: WalletMetaInfo, + item: TangemPayTxHistoryItem, + ) { + addUserRequestBody(walletMetaInfo) + addDelimiter() + addTangemPayTxInfo(item) + } + private suspend fun FeedbackDataBuilder.addVisaRequestBody( walletMetaInfo: WalletMetaInfo, visaTxDetails: VisaTxDetails, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 583f43cb02..7a76916fd1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -23,6 +23,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.Visa.Activation, is FeedbackEmailType.Visa.DirectUserRequest, is FeedbackEmailType.Visa.Dispute, + is FeedbackEmailType.Visa.DisputeV2, -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 490c6611dd..e930678eae 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -39,7 +39,9 @@ internal class EmailSubjectResolver(private val resources: Resources) { FeedbackEmailType.CardAttestationFailed -> "Card attestation failed" is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}" is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}" - is FeedbackEmailType.Visa.Dispute -> "[Visa] [DISPUTE] {auto-filled subject}" + is FeedbackEmailType.Visa.Dispute, + is FeedbackEmailType.Visa.DisputeV2, + -> "[Visa] [DISPUTE] {auto-filled subject}" } } } \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index f2d3d5a0cf..5cb3097c77 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) implementation(deps.kotlin.datetime) + implementation(deps.jodatime) implementation(deps.kotlin.serialization) ksp(deps.moshi.kotlin.codegen) implementation(deps.arrow.core) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/CurrencySerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/CurrencySerializer.kt new file mode 100644 index 0000000000..b17a17658f --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/CurrencySerializer.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.models.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.Currency + +typealias SerializedCurrency = @Serializable(with = CurrencySerializer::class) Currency + +object CurrencySerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.util.Currency", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Currency) { + encoder.encodeString(value.currencyCode) + } + + override fun deserialize(decoder: Decoder): Currency { + return Currency.getInstance(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/JodaDateTimeSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/JodaDateTimeSerializer.kt new file mode 100644 index 0000000000..d72729ef78 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/JodaDateTimeSerializer.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.models.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.joda.time.DateTime + +typealias SerializedDateTime = @Serializable(with = JodaDateTimeSerializer::class) DateTime + +object JodaDateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DateTime", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: DateTime) { + encoder.encodeLong(value.millis) + } + + override fun deserialize(decoder: Decoder): DateTime { + return DateTime(decoder.decodeLong()) + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt index 6793255df3..d5a102ee64 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt @@ -1,20 +1,25 @@ package com.tangem.domain.visa.model -import org.joda.time.DateTime -import java.math.BigDecimal -import java.util.Currency +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedCurrency +import com.tangem.domain.models.serialization.SerializedDateTime +import kotlinx.serialization.Serializable +@Serializable sealed class TangemPayTxHistoryItem { abstract val id: String - abstract val date: DateTime - abstract val amount: BigDecimal - abstract val currency: Currency + abstract val date: SerializedDateTime + abstract val amount: SerializedBigDecimal + abstract val currency: SerializedCurrency + abstract val jsonRepresentation: String + @Serializable data class Spend( override val id: String, - override val date: DateTime, - override val amount: BigDecimal, - override val currency: Currency, + override val jsonRepresentation: String, + override val date: SerializedDateTime, + override val amount: SerializedBigDecimal, + override val currency: SerializedCurrency, val enrichedMerchantName: String?, val merchantName: String, val enrichedMerchantCategory: String?, @@ -23,18 +28,23 @@ sealed class TangemPayTxHistoryItem { val enrichedMerchantIconUrl: String?, ) : TangemPayTxHistoryItem() + @Serializable data class Payment( override val id: String, - override val date: DateTime, - override val amount: BigDecimal, - override val currency: Currency, + override val jsonRepresentation: String, + override val date: SerializedDateTime, + override val amount: SerializedBigDecimal, + override val currency: SerializedCurrency, + val transactionHash: String?, ) : TangemPayTxHistoryItem() + @Serializable data class Fee( override val id: String, - override val date: DateTime, - override val amount: BigDecimal, - override val currency: Currency, + override val jsonRepresentation: String, + override val date: SerializedDateTime, + override val amount: SerializedBigDecimal, + override val currency: SerializedCurrency, ) : TangemPayTxHistoryItem() enum class Status { diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 428f063730..c78a18aa6f 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -2,9 +2,10 @@ package com.tangem.features.tangempay.components import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig interface TangemPayDetailsComponent : ComposableContentComponent { - data class Params(val config: TangemPayDetailsConfig) + data class Params(val userWalletId: UserWalletId, val config: TangemPayDetailsConfig) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts index cc2ac540ec..4789f8413a 100644 --- a/features/tangempay/details/impl/build.gradle.kts +++ b/features/tangempay/details/impl/build.gradle.kts @@ -15,8 +15,9 @@ dependencies { /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) - implementation(projects.core.ui) implementation(projects.core.error) + implementation(projects.core.navigation) + implementation(projects.core.ui) /** Features api */ implementation(projects.features.tangempay.details.api) @@ -26,9 +27,12 @@ dependencies { /** Domain */ implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) implementation(projects.domain.models) implementation(projects.domain.visa) implementation(projects.domain.visa.models) + implementation(projects.domain.wallets) /** Compose */ implementation(deps.compose.coil) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt index adad4cfd19..53e057a5c8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -15,8 +15,9 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent +import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent import com.tangem.features.tangempay.model.TangemPayDetailsModel -import com.tangem.features.tangempay.model.TangemPayDetailsNavigation +import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation import com.tangem.features.tangempay.ui.TangemPayDetailsScreen import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted @@ -39,7 +40,10 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( ) private val txHistoryComponent = DefaultTangemPayTxHistoryComponent( appComponentContext = child("txHistoryComponent"), - params = DefaultTangemPayTxHistoryComponent.Params(customerWalletAddress = params.config.customerWalletAddress), + params = DefaultTangemPayTxHistoryComponent.Params( + customerWalletAddress = params.config.customerWalletAddress, + uiActions = model, + ), ) @Composable @@ -59,19 +63,25 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( private fun bottomSheetChild( navigation: TangemPayDetailsNavigation, componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (navigation) { - is TangemPayDetailsNavigation.Error -> TangemPayErrorBottomSheetComponent( - appComponentContext = appComponentContext, - messageUM = navigation.messageUM, - onDismiss = model.bottomSheetNavigation::dismiss, - ) - is TangemPayDetailsNavigation.Receive -> tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = navigation.config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) + ): ComposableBottomSheetComponent { + val context = childByContext(componentContext) + return when (navigation) { + is TangemPayDetailsNavigation.Receive -> tokenReceiveComponentFactory.create( + context = context, + params = TokenReceiveComponent.Params( + config = navigation.config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + is TangemPayDetailsNavigation.TransactionDetails -> TangemPayTxHistoryDetailsComponent( + appComponentContext = context, + params = TangemPayTxHistoryDetailsComponent.Params( + transaction = navigation.transaction, + userWalletId = params.userWalletId, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } } @AssistedFactory diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayErrorBottomSheetComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayErrorBottomSheetComponent.kt deleted file mode 100644 index 62e7a375f5..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayErrorBottomSheetComponent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.tangempay.components - -import androidx.compose.runtime.Composable -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2 -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent - -internal class TangemPayErrorBottomSheetComponent( - appComponentContext: AppComponentContext, - private val messageUM: MessageBottomSheetUMV2, - private val onDismiss: () -> Unit, -) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { - - override fun dismiss() { - onDismiss() - } - - @Composable - override fun BottomSheet() { - MessageBottomSheetV2(state = messageUM, onDismissRequest = ::dismiss) - } -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt index 4338d0ecbc..42a85d03c4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM import com.tangem.features.tangempay.model.TangemPayTxHistoryModel import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems +import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions import kotlinx.coroutines.flow.StateFlow internal class DefaultTangemPayTxHistoryComponent( @@ -21,5 +22,5 @@ internal class DefaultTangemPayTxHistoryComponent( tangemPayTxHistoryItems(listState, state) } - data class Params(val customerWalletAddress: String) + data class Params(val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt new file mode 100644 index 0000000000..345ca4a497 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.tangempay.components.txHistory + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.* +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.model.TangemPayTxHistoryItem +import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel +import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContent + +internal class TangemPayTxHistoryDetailsComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayTxHistoryDetailsModel = getOrCreateModel(params = params) + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + TangemPayTxHistoryDetailsContent(state = model.uiState) + } + + data class Params( + val transaction: TangemPayTxHistoryItem, + val userWalletId: UserWalletId, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index ef72f17c45..cc9f121abb 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.tangempay.model.TangemPayDetailsModel +import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryModel import dagger.Binds import dagger.Module @@ -23,4 +24,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayTxHistoryModel::class) fun bindTangemPayTxHistoryModel(model: TangemPayTxHistoryModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayTxHistoryDetailsModel::class) + fun bindTangemPayTxHistoryDetailsModel(model: TangemPayTxHistoryDetailsModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt new file mode 100644 index 0000000000..4bf153edca --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -0,0 +1,15 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.visa.model.TangemPayTxHistoryItem +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class TangemPayDetailsNavigation { + + @Serializable + data class Receive(val config: TokenReceiveConfig) : TangemPayDetailsNavigation() + + @Serializable + data class TransactionDetails(val transaction: TangemPayTxHistoryItem) : TangemPayDetailsNavigation() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt new file mode 100644 index 0000000000..2ca794500d --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.ColorReference +import com.tangem.core.ui.extensions.ImageReference +import com.tangem.core.ui.extensions.TextReference + +internal data class TangemPayTxHistoryDetailsUM( + val title: TextReference, + val iconState: ImageReference, + val transactionTitle: TextReference, + val transactionSubtitle: TextReference, + val transactionAmount: String, + val transactionAmountColor: ColorReference, + val labelState: LabelUM?, + val notification: NotificationConfig?, + val buttonState: ButtonState, + val dismiss: () -> Unit, +) { + + data class ButtonState(val text: TextReference, val onClick: () -> Unit) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index da92a7606c..0a8a5676fe 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.toArgb import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -19,13 +18,16 @@ import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveType import com.tangem.domain.pay.DataForReceiveFactory import com.tangem.domain.pay.repository.CardDetailsRepository +import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType +import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.features.tangempay.model.transformers.* import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory +import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -53,7 +55,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val dataForReceiveFactory: DataForReceiveFactory, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, -) : Model() { +) : Model(), TangemPayTxHistoryUiActions { private val params: TangemPayDetailsComponent.Params = paramsContainer.require() @@ -102,11 +104,8 @@ internal class TangemPayDetailsModel @Inject constructor( bottomSheetNavigation.activate(TangemPayDetailsNavigation.Receive(config)) } .onLeft { - val messageUM = TangemPayErrorMessageFactory.createError( - type = TangemPayDetailsErrorType.Receive, - onDismiss = bottomSheetNavigation::dismiss, - ) - bottomSheetNavigation.activate(TangemPayDetailsNavigation.Error(messageUM)) + val messageUM = TangemPayErrorMessageFactory.createErrorMessage(TangemPayDetailsErrorType.Receive) + uiMessageSender.send(message = messageUM) } } } @@ -163,4 +162,8 @@ internal class TangemPayDetailsModel @Inject constructor( SnackbarMessage(TextReference.Res(R.string.tangempay_card_details_error_text)), ) } + + override fun onTransactionClick(item: TangemPayTxHistoryItem) { + bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item)) + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsNavigation.kt deleted file mode 100644 index bcf24b8829..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsNavigation.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.tangempay.model - -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.domain.models.TokenReceiveConfig -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class TangemPayDetailsNavigation { - - data class Receive( - val config: TokenReceiveConfig, - ) : TangemPayDetailsNavigation() - - data class Error( - val messageUM: MessageBottomSheetUMV2, - ) : TangemPayDetailsNavigation() -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt new file mode 100644 index 0000000000..48d190991e --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt @@ -0,0 +1,66 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.feedback.GetWalletMetaInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent +import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM +import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayTxHistoryDetailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletsUseCase: GetWalletsUseCase, + private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val urlOpener: UrlOpener, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + val uiState: TangemPayTxHistoryDetailsUM = TangemPayTxHistoryDetailsConverter.convert( + TangemPayTxHistoryDetailsConverter.Input( + item = params.transaction, + onExplorerClick = ::openExplorer, + onDisputeClick = ::dispute, + onDismiss = ::dismiss, + ), + ) + + fun dismiss() { + params.onDismiss() + } + + fun openExplorer(txHash: String?) { + txHash?.let(urlOpener::openUrlExternalBrowser) + } + + fun dispute() { + modelScope.launch { + val userWalletId = params.userWalletId + val userWallet = getUserWalletsUseCase.invokeSync() + .firstOrNull { it.walletId == userWalletId } ?: return@launch + val walletMetaInfo = getWalletMetaInfoUseCase.invoke( + userWallet.requireColdWallet().scanResponse, + ).getOrNull() ?: return@launch + + sendFeedbackEmailUseCase.invoke( + FeedbackEmailType.Visa.DisputeV2( + item = params.transaction, + walletMetaInfo = walletMetaInfo, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index ac67e1d5b4..85a8c31571 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -6,17 +6,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository -import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager -import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions import com.tangem.pagination.PaginationStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Stable @@ -26,14 +23,14 @@ internal class TangemPayTxHistoryModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, tangemPayTxHistoryRepository: TangemPayTxHistoryRepository, paramsContainer: ParamsContainer, -) : Model(), TangemPayTxHistoryUiActions { +) : Model() { private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require() private val listManager = TangemPayTxHistoryListManager( repository = tangemPayTxHistoryRepository, dispatchers = dispatchers, customerWalletAddress = params.customerWalletAddress, - txHistoryUiActions = this, + txHistoryUiActions = params.uiActions, ) val uiState: StateFlow @@ -115,10 +112,6 @@ internal class TangemPayTxHistoryModel @Inject constructor( .launchIn(modelScope) } - override fun onTransactionClick(item: TangemPayTxHistoryItem) { - Timber.d("onTransactionClick: $item") - } - private fun getEmptyState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Empty { return TangemPayTxHistoryUM.Empty(isBalanceHidden = isBalanceHidden) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt new file mode 100644 index 0000000000..edf1708a96 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt @@ -0,0 +1,202 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.visa.model.TangemPayTxHistoryItem +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isPositive + +internal object TangemPayTxHistoryDetailsConverter : + Converter { + private val dateFormatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM") + + override fun convert(value: Input): TangemPayTxHistoryDetailsUM { + val transaction = value.item + return TangemPayTxHistoryDetailsUM( + title = transaction.extractDate(), + iconState = transaction.extractIcon(), + transactionTitle = transaction.extractTransactionTitle(), + transactionSubtitle = transaction.extractTransactionSubtitle(), + transactionAmount = transaction.extractAmount(), + transactionAmountColor = value.item.extractAmountColor(), + labelState = value.item.extractLabel(), + notification = value.item.extractNotification(), + buttonState = value.extractButtonState(), + dismiss = value.onDismiss, + ) + } + + private fun TangemPayTxHistoryItem.extractDate(): TextReference { + val date = DateTimeFormatters.formatDate(this.date, dateFormatter) + val time = DateTimeFormatters.formatDate(this.date, DateTimeFormatters.timeFormatter) + + return stringReference("$date ${StringsSigns.DOT} $time") + } + + private fun TangemPayTxHistoryItem.extractIcon(): ImageReference { + return when (this) { + is TangemPayTxHistoryItem.Fee -> ImageReference.Res(R.drawable.ic_percent_24) + is TangemPayTxHistoryItem.Payment -> { + if (this.amount.isPositive()) { + ImageReference.Res(R.drawable.ic_arrow_down_24) + } else { + ImageReference.Res(R.drawable.ic_arrow_up_24) + } + } + is TangemPayTxHistoryItem.Spend -> { + val merchantIcon = this.enrichedMerchantIconUrl + if (merchantIcon != null) { + ImageReference.Url(merchantIcon) + } else { + ImageReference.Res(R.drawable.ic_category_24) + } + } + } + } + + private fun TangemPayTxHistoryItem.extractTransactionTitle(): TextReference { + return when (this) { + is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_title) + is TangemPayTxHistoryItem.Spend -> stringReference(this.enrichedMerchantName ?: this.merchantName) + is TangemPayTxHistoryItem.Payment -> if (this.amount.isPositive()) { + resourceReference(R.string.tangem_pay_deposit) + } else { + resourceReference(R.string.tangem_pay_withdrawal) + } + } + } + + private fun TangemPayTxHistoryItem.extractTransactionSubtitle(): TextReference { + return when (this) { + is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_subtitle) + is TangemPayTxHistoryItem.Payment -> resourceReference(R.string.common_transfer) + is TangemPayTxHistoryItem.Spend -> stringReference(this.enrichedMerchantCategory ?: this.merchantCategory) + } + } + + private fun TangemPayTxHistoryItem.extractAmount(): String { + return when (this) { + is TangemPayTxHistoryItem.Fee, + is TangemPayTxHistoryItem.Spend, + -> { + val amount = this.amount.format { + fiat( + fiatCurrencyCode = this@extractAmount.currency.currencyCode, + fiatCurrencySymbol = this@extractAmount.currency.symbol, + ) + } + StringsSigns.MINUS + amount + } + is TangemPayTxHistoryItem.Payment -> { + val amount = this.amount.format { + fiat( + fiatCurrencyCode = this@extractAmount.currency.currencyCode, + fiatCurrencySymbol = this@extractAmount.currency.symbol, + ) + } + if (this.amount.isPositive()) { + StringsSigns.PLUS + amount + } else { + StringsSigns.MINUS + amount + } + } + } + } + + private fun TangemPayTxHistoryItem.extractAmountColor(): ColorReference { + return when (this) { + is TangemPayTxHistoryItem.Fee, + is TangemPayTxHistoryItem.Spend, + -> themedColor { TangemTheme.colors.text.primary1 } + is TangemPayTxHistoryItem.Payment -> themedColor { + if (this.amount.isPositive()) { + TangemTheme.colors.text.accent + } else { + TangemTheme.colors.text.primary1 + } + } + } + } + + private fun TangemPayTxHistoryItem.extractLabel(): LabelUM? { + return when (this) { + is TangemPayTxHistoryItem.Fee, + is TangemPayTxHistoryItem.Payment, + -> null + is TangemPayTxHistoryItem.Spend -> when (this.status) { + TangemPayTxHistoryItem.Status.COMPLETED -> LabelUM( + text = resourceReference(R.string.tangem_pay_status_completed), + style = LabelStyle.ACCENT, + ) + TangemPayTxHistoryItem.Status.PENDING -> LabelUM( + text = resourceReference(R.string.tangem_pay_status_pending), + style = LabelStyle.REGULAR, + icon = com.tangem.core.ui.R.drawable.ic_clock_24, + ) + TangemPayTxHistoryItem.Status.DECLINED -> LabelUM( + text = resourceReference(R.string.tangem_pay_status_declined), + style = LabelStyle.WARNING, + ) + TangemPayTxHistoryItem.Status.RESERVED, + TangemPayTxHistoryItem.Status.UNKNOWN, + -> null + } + } + } + + private fun TangemPayTxHistoryItem.extractNotification(): NotificationConfig? { + return when (this) { + is TangemPayTxHistoryItem.Payment -> null + is TangemPayTxHistoryItem.Fee -> NotificationConfig( + title = resourceReference(R.string.tangem_pay_transaction_fee_notification_text), + subtitle = TextReference.EMPTY, + iconResId = R.drawable.ic_token_info_24, + ) + is TangemPayTxHistoryItem.Spend -> when (this.status) { + TangemPayTxHistoryItem.Status.DECLINED -> NotificationConfig( + title = resourceReference(R.string.tangem_pay_transaction_declined_notification_text), + subtitle = TextReference.EMPTY, + iconResId = R.drawable.ic_token_info_24, + ) + TangemPayTxHistoryItem.Status.PENDING, + TangemPayTxHistoryItem.Status.COMPLETED, + TangemPayTxHistoryItem.Status.RESERVED, + TangemPayTxHistoryItem.Status.UNKNOWN, + -> null + } + } + } + + private fun Input.extractButtonState(): TangemPayTxHistoryDetailsUM.ButtonState { + return when (this.item) { + is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryDetailsUM.ButtonState( + text = resourceReference(R.string.tangem_pay_dispute), + onClick = this.onDisputeClick, + ) + is TangemPayTxHistoryItem.Spend -> TangemPayTxHistoryDetailsUM.ButtonState( + text = resourceReference(R.string.tangem_pay_dispute), + onClick = this.onDisputeClick, + ) + is TangemPayTxHistoryItem.Payment -> TangemPayTxHistoryDetailsUM.ButtonState( + text = resourceReference(R.string.tangem_pay_explore_transaction), + onClick = { this.onExplorerClick(this.item.transactionHash) }, + ) + } + } + + data class Input( + val item: TangemPayTxHistoryItem, + val onExplorerClick: (String?) -> Unit, + val onDisputeClick: () -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/InternalComponents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/InternalComponents.kt new file mode 100644 index 0000000000..874fab6548 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/InternalComponents.kt @@ -0,0 +1,46 @@ +package com.tangem.features.tangempay.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import coil.compose.rememberAsyncImagePainter +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun RemoteIcon(url: String, modifier: Modifier = Modifier) { + Icon( + modifier = modifier.clip(CircleShape), + painter = rememberAsyncImagePainter(url), + contentDescription = null, + tint = Color.Unspecified, + ) +} + +@Composable +internal fun LocalStaticIcon(@DrawableRes id: Int, iconSize: Dp, modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.secondary.copy(alpha = 0.1F), + shape = CircleShape, + ), + ) { + Icon( + painter = painterResource(id), + contentDescription = null, + modifier = Modifier + .size(iconSize) + .align(Alignment.Center), + tint = TangemTheme.colors.icon.informative, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt index bcaec6f86e..deddf2a265 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt @@ -1,13 +1,11 @@ package com.tangem.features.tangempay.ui -import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -15,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign @@ -24,7 +21,6 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import coil.compose.rememberAsyncImagePainter import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -264,47 +260,24 @@ private fun Icon(state: TangemPayTransactionState, modifier: Modifier = Modifier if (state.iconUrl != null) { RemoteIcon(modifier = modifier, url = state.iconUrl) } else { - LocalStaticIcon(modifier = modifier, id = R.drawable.ic_category_24) + LocalStaticIcon( + modifier = modifier, + id = R.drawable.ic_category_24, + iconSize = TangemTheme.dimens.size20, + ) } } - is TangemPayTransactionState.Content.Fee -> LocalStaticIcon(modifier = modifier, id = R.drawable.ic_percent_24) + is TangemPayTransactionState.Content.Fee -> LocalStaticIcon( + modifier = modifier, + id = R.drawable.ic_percent_24, + iconSize = TangemTheme.dimens.size20, + ) is TangemPayTransactionState.Content.Payment -> LocalStaticIcon( modifier = modifier, id = if (state.isIncome) R.drawable.ic_arrow_down_24 else R.drawable.ic_arrow_up_24, + iconSize = TangemTheme.dimens.size20, ) - is TangemPayTransactionState.Loading -> { - CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40)) - } - } -} - -@Composable -private fun RemoteIcon(url: String, modifier: Modifier = Modifier) { - Icon( - modifier = modifier - .size(TangemTheme.dimens.size40) - .clip(CircleShape), - painter = rememberAsyncImagePainter(url), - contentDescription = null, - tint = Color.Unspecified, - ) -} - -@Composable -private fun LocalStaticIcon(@DrawableRes id: Int, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size40) - .background(color = TangemTheme.colors.icon.secondary.copy(alpha = 0.1F), shape = CircleShape), - ) { - Icon( - painter = painterResource(id), - contentDescription = null, - modifier = Modifier - .size(TangemTheme.dimens.size20) - .align(Alignment.Center), - tint = TangemTheme.colors.icon.informative, - ) + is TangemPayTransactionState.Loading -> CircleShimmer(modifier = modifier) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUi.kt new file mode 100644 index 0000000000..7d79f24a4e --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUi.kt @@ -0,0 +1,241 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.ImageReference +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM + +@Composable +internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.dismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = state.dismiss, + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = state.title, + endIconRes = R.drawable.ic_close_24, + onEndClick = state.dismiss, + ) + }, + ) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .padding(top = 24.dp) + .size(88.dp), + iconState = state.iconState, + ) + Text( + modifier = Modifier.padding(top = 32.dp), + text = state.transactionTitle.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.padding(top = 2.dp), + text = state.transactionSubtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + modifier = Modifier.padding(top = 8.dp), + text = state.transactionAmount, + style = TangemTheme.typography.head, + color = state.transactionAmountColor.resolveReference(), + ) + state.labelState?.let { Label(state = state.labelState, modifier = Modifier.padding(top = 12.dp)) } + SpacerH32() + state.notification?.let { + Notification( + config = state.notification, + titleColor = TangemTheme.colors.text.tertiary, + iconTint = TangemTheme.colors.icon.secondary, + ) + } + ButtonContainer( + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth(), + buttonState = state.buttonState, + ) + } + } +} + +@Composable +private fun ButtonContainer(buttonState: TangemPayTxHistoryDetailsUM.ButtonState, modifier: Modifier = Modifier) { + SecondaryButton(modifier = modifier, text = buttonState.text.resolveReference(), onClick = buttonState.onClick) +} + +@Composable +private fun Icon(iconState: ImageReference, modifier: Modifier = Modifier) { + when (iconState) { + is ImageReference.Res -> LocalStaticIcon(modifier = modifier, id = iconState.resId, iconSize = 40.dp) + is ImageReference.Url -> RemoteIcon(modifier = modifier, url = iconState.url) + } +} + +@Preview(device = Devices.PIXEL_7_PRO) +@Preview(device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayTxHistoryDetailsContentPreview( + @PreviewParameter(TangemPayTxHistoryDetailsUMProvider::class) state: TangemPayTxHistoryDetailsUM, +) { + TangemThemePreview { + TangemPayTxHistoryDetailsContent(state = state) + } +} + +private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterProvider( + listOf( + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_category_24), + transactionTitle = stringReference("Starbucks"), + transactionSubtitle = stringReference("Food and drinks"), + transactionAmount = "-$5.86", + transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 }, + labelState = LabelUM( + text = resourceReference(R.string.tangem_pay_status_pending), + style = LabelStyle.REGULAR, + icon = R.drawable.ic_clock_24, + ), + notification = null, + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Dispute"), + onClick = {}, + ), + dismiss = {}, + ), + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_category_24), + transactionTitle = stringReference("Starbucks"), + transactionSubtitle = stringReference("Food and drinks"), + transactionAmount = "-$5.86", + transactionAmountColor = themedColor { TangemTheme.colors.text.warning }, + labelState = LabelUM( + text = resourceReference(R.string.tangem_pay_status_declined), + style = LabelStyle.WARNING, + ), + notification = NotificationConfig( + title = stringReference("The bank rejected this transaction request."), + subtitle = TextReference.EMPTY, + iconResId = R.drawable.ic_token_info_24, + ), + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Dispute"), + onClick = {}, + ), + dismiss = {}, + ), + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_category_24), + transactionTitle = stringReference("Starbucks"), + transactionSubtitle = stringReference("Food and drinks"), + transactionAmount = "-$5.86", + transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 }, + labelState = LabelUM( + text = resourceReference(R.string.tangem_pay_status_completed), + style = LabelStyle.ACCENT, + ), + notification = null, + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Dispute"), + onClick = {}, + ), + dismiss = {}, + ), + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_percent_24), + transactionTitle = stringReference("Fee"), + transactionSubtitle = stringReference("Service fee"), + transactionAmount = "-$5.86", + transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 }, + labelState = null, + notification = NotificationConfig( + title = stringReference("This fee goes to cover the cost of handling your transfer."), + subtitle = TextReference.EMPTY, + iconResId = R.drawable.ic_token_info_24, + ), + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Dispute"), + onClick = {}, + ), + dismiss = {}, + ), + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_arrow_down_24), + transactionTitle = stringReference("Deposit"), + transactionSubtitle = stringReference("Transfers"), + transactionAmount = "+$20", + transactionAmountColor = themedColor { TangemTheme.colors.text.accent }, + labelState = null, + notification = null, + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Explore transaction"), + onClick = {}, + ), + dismiss = {}, + ), + TangemPayTxHistoryDetailsUM( + title = stringReference("12 June • 12:40"), + iconState = ImageReference.Res(R.drawable.ic_arrow_up_24), + transactionTitle = stringReference("Withdrawal"), + transactionSubtitle = stringReference("Transfers"), + transactionAmount = "-$5.86", + transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 }, + labelState = null, + notification = null, + buttonState = TangemPayTxHistoryDetailsUM.ButtonState( + text = stringReference("Explore transaction"), + onClick = {}, + ), + dismiss = {}, + ), + ), +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayErrorMessageFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayErrorMessageFactory.kt index 2f5ce481ea..6aa000667c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayErrorMessageFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayErrorMessageFactory.kt @@ -4,13 +4,15 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.BottomSheetMessageV2 +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType internal object TangemPayErrorMessageFactory { - fun createError(type: TangemPayDetailsErrorType, onDismiss: () -> Unit): MessageBottomSheetUMV2 { + fun createErrorMessage(type: TangemPayDetailsErrorType): BottomSheetMessageV2 { return when (type) { - TangemPayDetailsErrorType.Receive -> messageBottomSheetUM { + TangemPayDetailsErrorType.Receive -> bottomSheetMessage { infoBlock { icon(R.drawable.img_attention_20) { backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention @@ -20,7 +22,7 @@ internal object TangemPayErrorMessageFactory { } primaryButton { text = resourceReference(R.string.common_got_it) - onClick { onDismiss() } + onClick { closeBs() } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 00f34e6cdf..5d318bb9ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -375,7 +375,12 @@ internal class WalletModel @Inject constructor( value = info, onClickIssue = ::issueOrder, onClickKyc = innerWalletRouter::openTangemPayOnboarding, - openDetails = innerWalletRouter::openTangemPayDetails, + openDetails = { config -> + innerWalletRouter.openTangemPayDetails( + userWalletId = stateHolder.getSelectedWalletId(), + config = config, + ) + }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 8213172d17..4fed890be6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -109,8 +109,8 @@ internal class DefaultWalletRouter @Inject constructor( router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding)) } - override fun openTangemPayDetails(config: TangemPayDetailsConfig) { - router.push(AppRoute.TangemPayDetails(config)) + override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) { + router.push(AppRoute.TangemPayDetails(userWalletId = userWalletId, config = config)) } override fun openYieldSupplyBottomSheet( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index d869ca8b84..120a13a248 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -60,7 +60,7 @@ internal interface InnerWalletRouter { fun openTangemPayOnboarding() - fun openTangemPayDetails(config: TangemPayDetailsConfig) + fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) /** Open BS abput yield supply active and all money deposited in AAVE */ fun openYieldSupplyBottomSheet( From 201e493a48835ad4d12caa8cf0550cd94ccc3ffd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Oct 2025 15:58:09 +0500 Subject: [PATCH 6/9] Updated on 2026-08-14 --- .../utils/TangemPayTxHistoryUiManager.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt index 0c4fb3698e..d4fc29fe56 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt @@ -41,15 +41,25 @@ internal class TangemPayTxHistoryUiManager( val currentUiBatches = state.value.uiBatches val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList() + var previousLastDate: String? = null + for ((key, data) in newCurrencyBatches) { // Find if batch with same key exists val existingBatchIndex = batches.indexOfFirst { it.key == key } val shouldUpdateExisting = existingBatchIndex != -1 && currentUiBatches[existingBatchIndex].data.transactionItemsSizeNotEqual(data) + // Get last date of previous batch's data + if (key > 0) { + val prevBatch = newCurrencyBatches.find { it.key == key - 1 } + previousLastDate = prevBatch?.data?.lastOrNull()?.date?.millis?.toDateFormatWithTodayYesterday() + } else { + previousLastDate = null + } + // Case 1: Update existing batch if sizes differ if (shouldUpdateExisting) { - val items = generateUiItems(key, data) + val items = generateUiItems(key, data, previousLastDate) batches[existingBatchIndex] = Batch(key = key, data = items) continue } @@ -60,7 +70,7 @@ internal class TangemPayTxHistoryUiManager( } // Case 3: Create new batch - val items = generateUiItems(key, data) + val items = generateUiItems(key, data, previousLastDate) batches.add(Batch(key = key, data = items)) } @@ -70,6 +80,7 @@ internal class TangemPayTxHistoryUiManager( private fun generateUiItems( key: Int, data: List, + previousLastDate: String? = null, ): List { val items = mutableListOf() @@ -84,12 +95,15 @@ internal class TangemPayTxHistoryUiManager( val firstItem = data.first() val firstDate = firstItem.date.millis.toDateFormatWithTodayYesterday() - items.add( - TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle( - title = firstDate, - itemKey = UUID.randomUUID().toString(), - ), - ) + // Only add group title if different from previous batch's last date + if (firstDate != previousLastDate) { + items.add( + TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle( + title = firstDate, + itemKey = UUID.randomUUID().toString(), + ), + ) + } items.add( TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)), ) From b90d9bd69785b20feeaf867bba15fb1093ea43b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Oct 2025 18:36:51 +0300 Subject: [PATCH 7/9] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index b64f8659fc..4e92adf6ed 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit b64f8659fce65e7749d50923b32bfad13e6b8cac +Subproject commit 4e92adf6ed23e0e79f60ce769d921256cc257241 From 363646350cac498ad97da471be42a27be506ed63 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Oct 2025 12:45:15 +0500 Subject: [PATCH 8/9] Updated on 2026-08-14 --- .../pay/usecase/ProduceTangemPayInitialDataUseCase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt index b0f5cb8e91..bddeb01eaf 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ProduceTangemPayInitialDataUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.pay.usecase import arrow.core.Either -import arrow.core.raise.either +import arrow.core.Either.Companion.catch import com.tangem.domain.pay.repository.OnboardingRepository class ProduceTangemPayInitialDataUseCase( @@ -9,10 +9,10 @@ class ProduceTangemPayInitialDataUseCase( ) { suspend operator fun invoke(): Either { - return either { + return catch { val isDataProduced = repository.isTangemPayInitialDataProduced() if (isDataProduced) { - return@either Unit + return@catch Unit } else { repository.produceInitialData() } From e92de998f41eea0ae60c70cadaa286bbecaab713 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Oct 2025 07:51:05 +0000 Subject: [PATCH 9/9] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1cd201c597..f7e514619e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.30-1285" +tangemBlockchainSdk = "develop-1287" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.30-567" +tangemCardSdk = "develop-564" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^