From 9e2eb5710b503248c26e4d6d7dc734bd0dd50c26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Nov 2025 14:12:42 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 16 +- ...mPayGenerateAddressAndSignChallengeTask.kt | 10 +- .../tangem/tap/routing/utils/ChildFactory.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 2 +- .../configs/feature_toggles_config.json | 2 +- .../datasource/api/common/config/ApiConfig.kt | 2 + .../api/common/config/TangemPayAuth.kt | 53 ++++ .../tangem/datasource/api/pay/TangemPayApi.kt | 11 +- .../datasource/api/pay/TangemPayAuthApi.kt | 28 +++ .../GenerateNonceByCustomerWalletRequest.kt | 1 + .../GetTokenByCustomerWalletRequest.kt | 2 +- ...RefreshCustomerWalletAccessTokenRequest.kt | 2 +- .../response/CheckCustomerWalletResponse.kt | 9 + .../TangemPayGenerateNonceResponse.kt | 10 + .../response/TangemPayGetTokensResponse.kt | 12 + .../tangem/datasource/di/ApiConfigsModule.kt | 6 + .../com/tangem/datasource/di/NetworkModule.kt | 10 + .../config/environment/EnvironmentConfig.kt | 1 + .../converter/EnvironmentConfigConverter.kt | 1 + .../models/EnvironmentConfigModel.kt | 1 + .../local/preferences/PreferencesKeys.kt | 4 + .../datasource/local/visa/TangemPayStorage.kt | 8 +- .../api/common/config/ApiConfigTest.kt | 1 + .../managers/ProdApiConfigsManagerTest.kt | 16 ++ core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 2 + core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 13 +- core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 4 +- .../components/inputrow/InputRowImageBase.kt | 20 +- .../components/inputrow/InputRowImageInfo.kt | 2 +- .../ui/src/main/res/drawable/img_visa_36.webp | Bin 0 -> 4350 bytes data/visa/build.gradle.kts | 2 + .../DefaultTangemPayAuthDataSource.kt | 12 +- .../pay/repository/DefaultKycRepository.kt | 18 +- .../repository/DefaultOnboardingRepository.kt | 237 +++++++++--------- .../DefaultTangemPayCardDetailsRepository.kt | 35 +-- .../DefaultTangemPayTxHistoryRepository.kt | 31 ++- .../repository/TangemPayRequestPerformer.kt | 139 ++++++---- .../data/pay/util/TangemPayErrorConverter.kt | 2 + .../data/pay/util/TangemPayWalletsManager.kt | 3 +- .../visa/DefaultVisaAuthRemoteDataSource.kt | 58 +++-- domain/visa/build.gradle.kts | 1 + .../com/tangem/domain/visa/error/VisaError.kt | 1 + .../domain/visa/model/TangemPayAuthTokens.kt | 18 ++ .../visa/model/TangemPayInitialCredentials.kt | 2 +- .../pay/datasource/TangemPayAuthDataSource.kt | 4 +- .../tangem/domain/pay/model/CustomerInfo.kt | 10 +- .../pay/model/TangemPayCustomerInfoError.kt | 8 + .../domain/pay/repository/KycRepository.kt | 3 +- .../pay/repository/OnboardingRepository.kt | 22 +- .../TangemPayCardDetailsRepository.kt | 19 +- .../ProduceTangemPayInitialDataUseCase.kt | 10 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 105 +++++++- .../TangemPayTxHistoryRepository.kt | 2 + .../datasource/VisaAuthRemoteDataSource.kt | 8 +- .../com/tangem/features/kyc/KycComponent.kt | 3 +- .../tangem/features/kyc/DefaultKycModel.kt | 11 +- ...faultTangemPayDetailsContainerComponent.kt | 1 + .../components/TangemPayChangePinComponent.kt | 3 +- .../components/TangemPayDetailsComponent.kt | 1 + .../DefaultTangemPayTxHistoryComponent.kt | 7 +- .../model/TangemPayCardDetailsBlockModel.kt | 2 +- .../model/TangemPayChangePinModel.kt | 19 +- .../tangempay/model/TangemPayDetailsModel.kt | 37 ++- .../model/TangemPayTxHistoryModel.kt | 2 +- .../DetailsHiddenStateTransformer.kt | 2 +- .../utils/TangemPayTxHistoryListManager.kt | 4 +- .../onboarding/impl/build.gradle.kts | 3 + .../model/TangemPayOnboardingModel.kt | 55 ++-- .../wallet/model/TangemPayMainInfoManager.kt | 26 ++ .../wallet/child/wallet/model/WalletModel.kt | 97 ++++--- .../model/intents/TangemPayClickIntents.kt | 40 +-- .../utils/WalletWarningsAnalyticsSender.kt | 11 +- .../wallet/state/model/TangemPayState.kt | 6 + .../wallet/state/model/WalletNotification.kt | 18 ++ .../TangemPayRefreshNeededStateTransformer.kt | 21 ++ .../TangemPayUnavailableStateTransformer.kt | 16 ++ .../visa/TangemPayMainScreenBlock.kt | 7 +- .../components/visa/TangemPayRefreshBlock.kt | 77 ++++++ .../components/visa/TangemUnavailableBlock.kt | 75 ++++++ 82 files changed, 1150 insertions(+), 401 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGenerateNonceResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGetTokensResponse.kt create mode 100644 core/ui/src/main/res/drawable/img_visa_36.webp create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/TangemPayMainInfoManager.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemUnavailableBlock.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 ec16f63f50..3687dc92e7 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -9,7 +9,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext @@ -36,7 +36,7 @@ internal class DefaultTangemPayStorage @Inject constructor( ) } - private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } + private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { @@ -50,7 +50,7 @@ internal class DefaultTangemPayStorage @Inject constructor( } } - override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) = + override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) @@ -60,7 +60,7 @@ internal class DefaultTangemPayStorage @Inject constructor( ) } - override suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? = + override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? = withContext(dispatcherProvider.io) { secureStorage.get(createKey(customerWalletAddress)) ?.decodeToString(throwOnInvalidSequence = true) @@ -98,6 +98,14 @@ internal class DefaultTangemPayStorage @Inject constructor( secureStorage.delete(createOrderIdKey(customerWalletAddress)) } + override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId) { + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), true) + } + + override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? { + return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) + } + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = withContext(dispatcherProvider.io) { secureStorage.delete(createCustomerAddressKey(userWalletId)) 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 index 000ae515ec..5c813c1a93 100644 --- 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 @@ -17,6 +17,7 @@ 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.domain.wallets.builder.UserWalletIdBuilder import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -44,14 +45,17 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) - val derivationResult = runDerivationTask(session, wallet) - val address = when (derivationResult) { + val address = when (val derivationResult = runDerivationTask(session, wallet)) { is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error) is CompletionResult.Success -> generateAddressFromExtendedKey(derivationResult.data) } + val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey) val challenge = withContext(dispatchersProvider.io) { - visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address) + visaAuthRemoteDataSource.getCustomerWalletAuthChallenge( + customerWalletAddress = address, + customerWalletId = userWalletId.stringValue, + ) }.getOrElse { return CompletionResult.Failure(it.tangemError) } val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge) 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 6d24482b1f..5a48848579 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 @@ -668,7 +668,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Kyc -> { createComponentChild( context = context, - params = KycComponent.Params, + params = KycComponent.Params(route.userWalletId), componentFactory = kycComponentFactory, ) } 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 ab6a1ccc86..1428522f9f 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 @@ -438,7 +438,7 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object Kyc : AppRoute(path = "/kyc") + data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc") @Serializable data class YieldSupplyPromo( 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 cd3fce3406..1d456f1591 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 @@ -37,7 +37,7 @@ }, { "name": "TANGEM_PAY_ENABLED", - "version": "undefined" + "version": "5.31.0" }, { "name": "NEW_TOKEN_RECEIVE_ENABLED", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 8b69a14c9d..8e0a661871 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -26,6 +26,7 @@ sealed class ApiConfig { StakeKit, P2PEthPool, TangemPay, + TangemPayAuth, BlockAid, YieldSupply, MoonPay, @@ -39,6 +40,7 @@ sealed class ApiConfig { is StakeKit -> ID.StakeKit is P2PEthPool -> ID.P2PEthPool is TangemPay -> ID.TangemPay + is TangemPayAuth -> ID.TangemPayAuth is BlockAid -> ID.BlockAid is YieldSupply -> ID.YieldSupply is MoonPay -> ID.MoonPay diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt new file mode 100644 index 0000000000..cbbc29459d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt @@ -0,0 +1,53 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.BuildConfig +import com.tangem.utils.ProviderSuspend +import com.tangem.utils.version.AppVersionProvider + +internal class TangemPayAuth( + private val appVersionProvider: AppVersionProvider, +) : ApiConfig() { + override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() + + override val environmentConfigs = listOf( + createDevEnvironment(), + createMockedEnvironment(), + createProdEnvironment(), + ) + + private fun getInitialEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + -> ApiEnvironment.DEV + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + + private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, + baseUrl = "https://api.dev.us.paera.com/", + headers = createHeaders(), + ) + + private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.MOCK, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.us.paera.com/", + headers = createHeaders(), + ) + + private fun createHeaders() = mapOf( + "version" to ProviderSuspend { appVersionProvider.versionName }, + "platform" to ProviderSuspend { "Android" }, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 044c788bc7..116560b40e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -26,11 +26,6 @@ interface TangemPayApi { @Body request: GenerateNoneByCardWalletRequest, ): ApiResponse - @POST("v1/auth/challenge") - suspend fun generateNonceByCustomerWallet( - @Body request: GenerateNonceByCustomerWalletRequest, - ): ApiResponse - @POST("v1/auth/token") suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse @@ -124,6 +119,12 @@ interface TangemPayApi { @GET("v1/customer/me") suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse + @GET("v1/customer/wallets/{customer_wallet_id}") + suspend fun checkCustomerWalletId( + @Header("X-API-KEY") authHeader: String, + @Path("customer_wallet_id") customerWalletId: String, + ): ApiResponse + @POST("v1/deeplink/validate") suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt new file mode 100644 index 0000000000..fbf512024c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayAuthApi.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.api.pay + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessTokenRequest +import com.tangem.datasource.api.pay.models.response.TangemPayGenerateNonceResponse +import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface TangemPayAuthApi { + + @POST("auth/challenge") + suspend fun generateNonceByCustomerWallet( + @Body request: GenerateNonceByCustomerWalletRequest, + ): ApiResponse + + @POST("auth/token") + suspend fun getTokenByCustomerWallet( + @Body request: GetTokenByCustomerWalletRequest, + ): ApiResponse + + @POST("auth/token/refresh") + suspend fun refreshCustomerWalletAccessToken( + @Body request: RefreshCustomerWalletAccessTokenRequest, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt index edd8260545..882f725799 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class GenerateNonceByCustomerWalletRequest( @Json(name = "auth_type") val authType: String = "customer_wallet", @Json(name = "customer_wallet_address") val customerWalletAddress: String, + @Json(name = "customer_wallet_id") val customerWalletId: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt index 9a5e47e327..0001d3a3f0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class GetTokenByCustomerWalletRequest( - @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "auth_type") val authType: String, @Json(name = "session_id") val sessionId: String, @Json(name = "signature") val signature: String, @Json(name = "message_format") val messageFormat: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshCustomerWalletAccessTokenRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshCustomerWalletAccessTokenRequest.kt index d4b509360f..d5d297612f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshCustomerWalletAccessTokenRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/RefreshCustomerWalletAccessTokenRequest.kt @@ -5,6 +5,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RefreshCustomerWalletAccessTokenRequest( - @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "auth_type") val authType: String, @Json(name = "refresh_token") val refreshToken: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt new file mode 100644 index 0000000000..11f891c668 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CheckCustomerWalletResponse( + @Json(name = "id") val id: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGenerateNonceResponse.kt new file mode 100644 index 0000000000..95d6460001 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGenerateNonceResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TangemPayGenerateNonceResponse( + @Json(name = "nonce") val nonce: String, + @Json(name = "session_id") val sessionId: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGetTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGetTokensResponse.kt new file mode 100644 index 0000000000..8fb5eaeb64 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayGetTokensResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TangemPayGetTokensResponse( + @Json(name = "access_token") val accessToken: String, + @Json(name = "expires_at") val expiresAt: Long, + @Json(name = "refresh_token") val refreshToken: String, + @Json(name = "refresh_expires_at") val refreshExpiresAt: Long, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 052decff8c..7bf7ad5ce5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -80,6 +80,12 @@ internal object ApiConfigsModule { @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider) + @Provides + @IntoSet + fun provideTangemPayAuthConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPayAuth( + appVersionProvider, + ) + @Provides @IntoSet fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index dd2041b5f1..6c07212416 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -17,6 +17,7 @@ import com.tangem.datasource.api.news.NewsApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.TangemPayAuthApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.YieldSupplyApi @@ -135,6 +136,15 @@ internal object NetworkModule { ) } + @Provides + @Singleton + fun provideTangemPayAuthApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayAuthApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemPayAuth, + applyTimeoutAnnotations = false, + ) + } + @Provides @Singleton fun provideBlockAidApi(retrofitApiBuilder: RetrofitApiBuilder): BlockAidApi { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 1d4c3967de..cce8979a7c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -24,4 +24,5 @@ data class EnvironmentConfig( val tangemApiKeyStage: String? = null, val yieldModuleApiKey: String? = null, val yieldModuleApiKeyDev: String? = null, + val bffStaticToken: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt index e75bd8c8a3..d8ec470eff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt @@ -32,6 +32,7 @@ internal object EnvironmentConfigConverter : Converter is stored. @@ -176,6 +177,9 @@ object PreferencesKeys { fun getTangemPayAddToWalletKey(customerWalletAddress: String) = booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress") + fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) = + booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId") + // endregion } 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 d74074f011..63f65c3ff3 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,16 +1,16 @@ package com.tangem.datasource.local.visa import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.domain.visa.model.TangemPayAuthTokens interface TangemPayStorage { suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? - suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) + suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) - suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? + suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? suspend fun storeOrderId(customerWalletAddress: String, orderId: String) @@ -21,6 +21,8 @@ interface TangemPayStorage { suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) + suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId) + suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index 693c666ff7..d92d9ff26e 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -74,6 +74,7 @@ class ApiConfigTest { ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk()) ApiConfig.ID.News -> News(authProvider = appAuthProvider) + ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = mockk()) } } } diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index bac344e79b..37f48a1f83 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -115,6 +115,7 @@ internal class ProdApiConfigsManagerTest { ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider) ApiConfig.ID.News -> News(authProvider = appAuthProvider) + ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = appVersionProvider) } } } @@ -130,6 +131,7 @@ internal class ProdApiConfigsManagerTest { ApiConfig.ID.MoonPay -> createMoonPayModel() ApiConfig.ID.P2PEthPool -> createP2PModel() ApiConfig.ID.News -> createNewsModel() + ApiConfig.ID.TangemPayAuth -> createTangemPayAuthModel() } } @@ -257,6 +259,20 @@ internal class ProdApiConfigsManagerTest { ) } + private fun createTangemPayAuthModel(): TestModel { + return TestModel( + id = ApiConfig.ID.TangemPayAuth, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, + baseUrl = "https://api.dev.us.paera.com/", + headers = mapOf( + "version" to ProviderSuspend { VERSION_NAME }, + "platform" to ProviderSuspend { "Android" }, + ), + ), + ) + } + private fun createBlockAidSdkModel(): TestModel { return TestModel( id = ApiConfig.ID.BlockAid, diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 79198ec916..bb6de8a19b 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -54,7 +54,7 @@ Bist Du sicher, dass Du das neue Konto verwerfen willst? Bist Du sicher, dass Du die Bearbeitungen verwerfen willst? Nicht gespeicherte Änderungen - Einige benutzerdefinierte Token wurden von „%1$s\" Zu \"%2$sda ihre Herleitung zu diesem Konto gehört. + Einige benutzerdefinierte Token wurden von “%1$s” Zu “%2$s” da ihre Herleitung zu diesem Konto gehört. Einige benutzerdefinierte Token wurden verschoben. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index a944d94513..039fe17b62 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -652,6 +652,8 @@ La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. Para crear una cuenta, envíe fondos a esta dirección La cuenta de destino no tiene una Trustline (línea de confianza) para el activo que se envía. + Obtén $10 en BTC con cada billetera \n ¡Date prisa! + Black Friday: hasta 30% DESCUENTO Únase ahora Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento. ¡Obtenga RECOMPENSAS por cada amigo! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 6b9fd987ef..5f2e9a67e9 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1386,7 +1386,7 @@ Code d\'erreur : %s. Si le problème persiste, n\'hésitez pas à contacter notre service d\'assistance. Si le problème persiste, n’hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue. - Le portefeuille Tangem ne prend actuellement pas en charge %ss + Le portefeuille Tangem ne prend actuellement pas en charge %s dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 4df1c486bf..84d5b13938 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -138,10 +138,17 @@ 残高は非表示 ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに! ベータモード + デバイスで生体認証がオフになっているため、ウォレットのロック解除に使用できません。\n生体認証を再度利用するには、デバイスの設定で有効にしてください。 + 生体認証が無効になっています カードまたはリングをスキャンしてください + 生体認証の試行回数が上限に達しました。端末タップまたはアクセスコードでウォレットを解除してください。 + 生体認証がロックされています 30秒後に再試行するか、カードまたはリングをスキャンしてください + 生体認証ログインは一時的にロックされています。30秒後にもう一度お試しいただくか、端末タップまたはアクセスコードでウォレットを解除してください。 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 + デバイスの生体認証が更新されました。再び生体認証ログインを有効にするため、ウォレットを選び、アクセスコードを入力してください。 + 対応が必要です プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。 アクティベーションエラー プロモーションコードが正常に有効化されました。報酬は14日以内にビットコインアカウントに入金されます。 @@ -818,6 +825,8 @@ 送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。 アカウントを作成するには、このアドレスに資金を送金してください 送信先アカウントには、送金されるアセットのトラストラインがありません。 + ウォレットごとに$10相当のBTCをプレゼント\nお早めに! + ブラックフライデー:最大30% オフ 今すぐ参加 コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 友達への紹介で報酬を獲得しよう! @@ -1177,7 +1186,7 @@ ステーキング金額は %s 以上である必要があります ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 - APR %1$s% + APR %1$s%% ステーキング解除分を請求する ステーキングアカウント手数料 ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 @@ -1869,7 +1878,7 @@ 今後の追加入金ごとにおおよそのネットワーク手数料%1$s ( %2$s ) が差し引かれ、 %3$s ( %4$s ) の制限を超えることはありません。 ネットワーク手数料が上限を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。 最大手数料 - 最小金額は現在のネットワーク手数料に基づいて計算され、入金額の4% 、つまり最小の%1$s ( %2$s ) を超えないようにします。 + 最小金額は現在のネットワーク手数料に基づいて計算され、入金額の4%% 、つまり最小の%1$s ( %2$s ) を超えないようにします。 最低入金額 入金手数料ポリシー Tangemは、生成された利息に対して15%サービス手数料も徴収します。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 3d1a09fd10..6288a0dc4c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -831,6 +831,8 @@ Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Для создания аккаунта отправьте средства на этот адрес Аккаунт получателя не содержит трастлайна для отправляемого актива. + Получите $10 в BTC за каждый кошелёк\nПоторопитесь! + Чёрная пятница: скидки до 30% Присоединиться Поделись промокодом — заработай 5 USDT с каждой покупки. Твои друзья получат скидку 10% на карту Tangem! Получай бонусы за каждого друга! diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8967ea964b..3d616fb516 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -842,8 +842,8 @@ Destination account is not active. Send %s or more to activate the account. To create account send funds to this address The destination account does not have a trustline for the asset being sent. - Plus Rewards in BTC per Set.\nHurry up - Black Friday: up to 25% OFF + Get $10 in BTC with every wallet\nHurry! + Black Friday: up to 30% OFF Join Now Share your code - earn 5 USDT per sale. Your friend gets 10% OFF. Get REWARDS for every friend! diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 36c14965b1..322fb14ef4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components.inputrow import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -15,6 +16,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R @@ -35,7 +37,7 @@ import com.tangem.core.ui.res.TangemThemePreview * @param modifier modifier * @param caption caption text * @param imageUrl icon to load - * @param iconRes icon resource + * @param iconResVector icon resource * @param subtitleColor subtitle text color * @param captionColor caption text color * @param iconTint icon tint @@ -47,12 +49,13 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Suppress("LongMethod") @Composable -internal fun InputRowImageBase( +fun InputRowImageBase( subtitle: TextReference, modifier: Modifier = Modifier, caption: TextReference? = null, imageUrl: String? = null, - @DrawableRes iconRes: Int? = null, + @DrawableRes iconResVector: Int? = null, + @DrawableRes iconResWebp: Int? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, iconTint: Color = TangemTheme.colors.icon.informative, @@ -76,7 +79,7 @@ internal fun InputRowImageBase( .clip(TangemTheme.shapes.roundedCornersXLarge), ) SpacerW12() - } else if (iconRes != null) { + } else if (iconResVector != null) { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -85,13 +88,20 @@ internal fun InputRowImageBase( .background(iconTint.copy(alpha = 0.08f)), ) { Icon( - painter = rememberVectorPainter(image = ImageVector.vectorResource(id = iconRes)), + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = iconResVector)), tint = iconTint, contentDescription = null, modifier = Modifier.size(TangemTheme.dimens.size18), ) } SpacerW12() + } else if (iconResWebp != null) { + Image( + painter = painterResource(iconResWebp), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size36), + ) + SpacerW12() } Column { Row { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index b5fbc11286..6daada836b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -83,7 +83,7 @@ fun InputRowImageInfo( subtitle = subtitle, caption = caption, imageUrl = imageUrl, - iconRes = iconRes, + iconResVector = iconRes, iconTint = iconTint, subtitleColor = subtitleColor, captionColor = captionColor, diff --git a/core/ui/src/main/res/drawable/img_visa_36.webp b/core/ui/src/main/res/drawable/img_visa_36.webp new file mode 100644 index 0000000000000000000000000000000000000000..dddc1857409ab700ea717e27b3ff302387c7078b GIT binary patch literal 4350 zcmVi&L6abx*X4!+vC@BUHiMf{>lntOA*LlW{%gJbo*~ze?4xO({X7#*Td=l>|cV3 zs+iCWI6&L;b~@}-j@LC7!dzlmKPZQ7yZ@-a{^uI$r$n7GBQgBsKF_sl*OwG0XbGVc zLAZSp;_BM3b>-9zKR*^?INe;ZWgv~b{hyk$tOm6@q_8Uaz~Fi=1?A_Qq!}7klxvz)~i-j?m8UJ<^BhOJa$U#ifH%*~{UtZ9Kv&K>XEhrccBt z*fj)#8Xwz^dOPTHm+&|O+8%s9OlI?A-}D?}k>%HlF)J}T4lV<+2Yyi}y>wpq54b(C zp)v^}$UR`p%r9}y(-3wnw9ss#-N-*(AuCa)YrHSr2U)Dy9Upea$}*jz(qnzAM^g*l z2r%7=;x$=&(BRe13KGuXm^g|yj2+=M_!mL9Z)M7)z?%qjF0X@q95)ezCUU0YCqpB# zc`@$r=?)|<;&Hk9%v?CXQ>5spMt*oHrbzI3ND$|qL5*{KmybZf(#z#YHa^S3R10x$ z^9!7m327ocj)^sW17*yxd5FcL^Froy*yENB>1G_dE90+=Pmzo-fo4KOys?YVwJA@n z-A0B4DxQzUfs;5+b7WJw8?W$zAIYHz$VElvIu60_*0yZ_5~^BgXkjE(>X0?qdTW`P zJV#B;kd-6>_vp62`~28 z^?XTIIlBOg`8xh@^Xm2jb^$p}blH7jpxB1eX%uxN9cm0q$7Y7{Bz&NBCP+CScCW0g_J7k?QNn+zLPTBb5VBflXsOXrgh4_wvC0>2sR*~%G z3GArb*u|dR14QRm#Ni^5w4AW04K=Hs)+`)87>*$d2~-TpU00=&h2zlRu9qrmoBakp z^~7X%mQiI9vy|-677{37XYrBqrXrGbh6HQum2g=syBXjPj1URqr;ThXC4%_4AYmf= zgmlGPeg%n#BEhwxyd+p__-%D7GgG7h&8MI3{BFa$G-zo?*y$Ef#usyClJo#dScQA# z!wgn>MOr-pC5quMvv zhY|369YuH9f3LD5iMac(d{ta?_yRZq!8hs!iUY&X>zfoRvGEeBXQ7}czR!3hO#&5p znrL}4A`Zs=lq9O`eOk-TiKdVjm)WJk@YE362+j9$9ef!_FW(KdJ-W63pO= z;ks*$*vzB3}4e{_w2THE+Mp~pX?<61~|s~pLo})FhN@M0fu0}L{l6XXpGDb;f%CC z9UBAOyWR*z!x1r%QH$Z;b$vv^D+#XYtEis|5nX=3(~_wV_nV~%E>CyLlBPN%6=~1{16jc z18OLzV+5|40}Bs=U0w^+^yzVlN#;xxwdbG+^QmA%U_dNw_K(nfADvD0i&_jGj46A% zuC=bA$#@mc*_g+)fb4;-AW zeU+O?w?I8KAom34UQ<5J$GaR02uV+HQ8ZCNNLa@2tk{p%rp=L&8uKjMBug522ntEl z+fXhez0=h)n1ZQ3YKio*z)ql_$8z|S3Y}>S=lU3JzCppHmqPAss@E?82Q8E?*`4eF zV#2^qb6)3i=p&)#L!OY22M-0q!p7|_sHd9Ol$pEcA%roQvU2CBv^B<^MM!-d`NY6i z)I>N6=7uB;mt&0iBYO&&T~`sZuvd`Q zx;#^P2Ub+%6j*2KI-Qo@*^`6Qhr+-#B2#_TPpz!p`T(uueUwnO_Eq`1&TZ~@f|GP6 zC{2dMUN47MU4MLRRTm*!LU)=0x!#U#pL#gmp8wPS7#Nsg+5d0!yq^wj=eitEm)qm9 zuItvn>L0BpwRwNfIIsS$>-u~=u0PMmrNFWQ09H^qAn*qO0FXEUodGJ40FVGa84QI% zp#*-LK?DF{Eg;W&wC;A9jf(f_fxW4%zZEVZ{z3Yud$!w z2kDye9A050x={+gkO0%+|5oL-xufUW zT+f_-R~lE89Ro5XNBt+qdWb8*gV-&RuV~QQV;S9TPys{!;~x({tK<%pkfp^+>ik~1 z=pU-7qZKrHZ>Iy$`wYw3^4&UWI)d5ItomK}U-${4?XD|qYgNgLXdBMUy3VSvhvr^lvi8U0xik1_AmEN_k zneYp7vkd8yANkEgg(WVK-10#@qFC>6_i}JuUw{Dq{^UR(K-U-2p0hu6v{enjKthTp z?2?r+LvHCeeX#^`ilbKDvLBzES`Hpj8nmT!24WowATdB6IQ*&cG={_OaN^l2QB9bk z2cB(eE%6^l4=vF!gfKF3S#g9)n@Y1PCQ<5F_#C)#l`Y8-cFm3iGg>Htco&HaenjI% zjdx$a@6XAr#lk57mq^`YLtHq~MNK2xX^&Ym7dneSi=MsI5NwtZ%qof;L8fq98#bmW z3jG2 zQo^V>U(MTWbYR{g!`=m2?+5Ky+S27wlGhvS`2BzLz@}`|$7a(RYy8BDlIyL2X!LYN z3DD`2@v;ov!aMUlUE%wixi~q6-I!7aTZ9`DW(>Z#>m~%Ap<-OVBzEO*4(w$^w%$Yy zo1~TSxq+H6f^9^KrNJ*X35Im(w;GZ2VJZ|E5%msAyD)knH4tzxQ$WR*cx@IxoQKxA znMxMh!pT)rYXk&RUC_`#B1a|IkN(J<(w|Z$jZx^e-+NeYGDAt$JSqx+7h@`UW#9QO zXmkD*A_IM_B8Z)zy4c#uDRdUaKw{6Eca{5F2?QX|I)JWloYFE;GQ2sgg>!UZ93m~)bt5;bp!roqWjEvpU^ zo8-Z%nJp!LPS@N!k!6Ib7gh+-syQn3J##SK_c)5#t7$mCFce|)-P$|Oj{87RB z%Ra9b*6kz7_O_eNViU}AHyoC$6)Dit>8FC6Ld6&gCA}D$%viv}I@)=uaeSk=Kx7VO zn(If$f2`wGm07SDE@*C_|56{`^EVLX615+llu&PrJZxhJwVQzs_e+E?B-_8WO&HQj z<_FJ!ayx}OekPO>7Ta|w#T^!t;Y%-uU##C58pWrvGn5gy{Uy8JfNDl~L{=$mNAA}* z3TkP?QQCEscz6BxGefbq)Mc>U8JOsS7?x0zgI^O5E9LY?(2&9rGG>IahUL*BNvkct z_DC)Lf^ps;wZ?XV$opUa!_7_ga)pEq_+)~~EEn(BxFJrDW7nDM8VBAP9(Huz1dj_z zazzD(Qf#7MCSEb9SgycEQqk#`(ogNB+X7@*{J;4jBl_Gc-&xP(m6fN|o~w)7dF3%K z?bu3knHm4~{~7}}1vmWJ%Mn!=JjDBTJQqR@KP|H{6HtU~`tKI^AHvtFr*d1xNZ|b% zrcQOT9f@N0KEpNiH8cjNC{w74v<|^j-`M>gPokGw;=vGs;AaquU`9pk+E}~ZUr@{G zfw=-6_foBT_ppBPz*7#2#Ku51ob;C4KZmE&rSo$q-s4sjdV{6A4X93Rl57Nvv&weE z7(kTx;;SdqhSjo7YowZPzBz|Gj8n^y?WPGgKu{jwuT0M9+`T$zGR~(#=5Ss}QIB!( zW)L6qPyX)ZX{c;lAwHV7&Kh1&f@m#%OVbzQ*5c5GfP>BWEK_f7JFSNpIl7|_A|>LP zqM(Flrgx(_aGvCqg0EVHYoU^ zUz4+bH4i=NXLs_nn9#ghaSgvv-i zdzBDE6nD*PYK74Lz*C6F*c~C?w?`v`40t_?&Y$HoW1`*D0RCZ>!XYyni&Ld#_@mzE zDd){xX6rc4Y@cWwlGtNnO1RRkp$Dfo4KJ7eulNX?-lclXNDWuS4^|T^kW_h2&btW1 sNfs)m`8}%~I5p#x8nVs*xM2W*kn-bcX0BvBul`PGfF5Q2`Fznp0FE+n%m4rY literal 0 HcmV?d00001 diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 0e59eaa831..287bd65478 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.visa) implementation(projects.domain.card) implementation(projects.domain.wallets) + implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) @@ -31,6 +32,7 @@ dependencies { implementation(projects.domain.networks) implementation(projects.domain.walletManager) implementation(projects.domain.quotes) + implementation(projects.domain.common) /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) 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 bca25d2bb9..32c978f1b1 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 @@ -5,8 +5,8 @@ import arrow.core.raise.either import com.tangem.common.CompletionResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.model.TangemPayAuthTokens 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,18 +16,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( ) : TangemPayAuthDataSource { override suspend fun produceInitialCredentials(cardId: String): Either { - val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId) - - return when (initialCredentials) { + return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) { is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error) is CompletionResult.Success -> Either.Right(initialCredentials.data) } } - override suspend fun refreshAuthTokens(refreshToken: String): Either = either { - visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens( - VisaAuthTokens.RefreshToken(refreshToken, authType = VisaAuthTokens.RefreshToken.Type.CardWallet), - ) + override suspend fun refreshAuthTokens(refreshToken: String): Either = either { + visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(refreshToken = refreshToken) .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .bind() } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt index e7c91d654c..83b97182fe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultKycRepository.kt @@ -1,26 +1,22 @@ package com.tangem.data.pay.repository import arrow.core.Either -import com.tangem.core.error.UniversalError import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository +import com.tangem.domain.visa.error.VisaApiError import javax.inject.Inject -private const val TAG = "TangemPay: KycRepository" - internal class DefaultKycRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, ) : KycRepository { - override suspend fun getKycStartInfo(): Either { - return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request { authHeader -> - tangemPayApi.getKycAccess(authHeader = authHeader) - }.result - - KycStartInfo(token = result.token, locale = result.locale) - } + override suspend fun getKycStartInfo(userWalletId: UserWalletId): Either { + return requestHelper.performRequest( + userWalletId, + ) { authHeader -> tangemPayApi.getKycAccess(authHeader = authHeader) } + .map { KycStartInfo(token = it.result.token, locale = it.result.locale) } } } \ 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 908b37cb4b..c6fa9b8d4e 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 @@ -1,26 +1,29 @@ 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.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId 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 -import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject private const val VALID_STATUS = "valid" @@ -34,105 +37,82 @@ internal class DefaultOnboardingRepository @Inject constructor( private val requestHelper: TangemPayRequestPerformer, private val tangemPayStorage: TangemPayStorage, private val authDataSource: TangemPayAuthDataSource, - private val tangemPayWalletsManager: TangemPayWalletsManager, private val cardFrozenStateStore: TangemPayCardFrozenStateStore, + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : OnboardingRepository { // Save data for a session - private var lastFetchedCustomerInfo: CustomerInfo? = null + private val lastFetchedCustomerInfoMap = ConcurrentHashMap() - override suspend fun validateDeeplink(link: String): Either { - return requestHelper.runWithErrorLogs(TAG) { - val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) - .getOrThrow() - .result - result?.status == VALID_STATUS + override suspend fun validateDeeplink(link: String): Either { + return requestHelper.performWithStaticToken { + tangemPayApi.validateDeeplink(body = DeeplinkValidityRequest(link = link)) + }.map { response -> + response.result?.status == VALID_STATUS } } - override suspend fun isTangemPayInitialDataProduced(): Boolean { - val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId - val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false - tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false + override suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean { + return withContext(dispatcherProvider.io) { + val customerWalletAddress = + tangemPayStorage.getCustomerWalletAddress(userWalletId) ?: return@withContext false + tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return@withContext false - return true + return@withContext 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 }, + override suspend fun produceInitialData(userWalletId: UserWalletId) { + withContext(dispatcherProvider.io) { + val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId)) + .fold( + ifLeft = { error -> error("Can not produce initial data: ${error.message}") }, + ifRight = { it }, + ) + // should storeCheckCustomerWalletResult because we already know this + tangemPayStorage.storeCheckCustomerWalletResult(userWalletId) + tangemPayStorage.storeCustomerWalletAddress( + userWalletId = userWalletId, + customerWalletAddress = initialCredentials.customerWalletAddress, + ) + tangemPayStorage.storeAuthTokens( + customerWalletAddress = initialCredentials.customerWalletAddress, + tokens = initialCredentials.authTokens, ) - 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 -> - tangemPayApi.getCustomerMe(authHeader) - }.result - getCustomerInfo(result) } } - override suspend fun getMainScreenCustomerInfo(): Either { - return requestHelper.runWithErrorLogs(TAG) { - val customerWalletAddress = requestHelper.getCustomerWalletAddress() + override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either { + // TODO implement selector + return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) } + .map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) } + } - when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) { - // If order id wasn't saved -> start order creation and get customer info - null -> { - createOrder() - MainScreenCustomerInfo( - info = getCustomerInfoWithPersistedToken(), - orderStatus = OrderStatus.UNKNOWN, - ) - } - // If order id was saved -> check its status - else -> { - val orderStatus = getOrderStatus(orderId) - if (orderStatus == OrderStatus.CANCELED) { - // If order was cancelled -> start order creation - createOrder() - } - val customerInfo = when (orderStatus) { - // Kyc is passed and user waits for order creation -> no need to get customer info - OrderStatus.NEW, - OrderStatus.PROCESSING, - -> CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null) - - // Order was created/cancelled -> clear order id and get customer info - OrderStatus.UNKNOWN, - OrderStatus.COMPLETED, - OrderStatus.CANCELED, - -> getCustomerInfoWithPersistedToken().also { - tangemPayStorage.clearOrderId(customerWalletAddress) - } - } - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) - } - } + override suspend fun clearOrderId(userWalletId: UserWalletId) { + withContext(dispatcherProvider.io) { + val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId) + tangemPayStorage.clearOrderId(customerWalletAddress = customerWalletAddress) } } - override fun getSavedCustomerInfo(): CustomerInfo? { - return lastFetchedCustomerInfo + override suspend fun getOrderId(userWalletId: UserWalletId): String? { + return withContext(dispatcherProvider.io) { + val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId) + tangemPayStorage.getOrderId(customerWalletAddress) + } } - private suspend fun createOrder() = withContext(dispatcherProvider.io) { + override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? { + return lastFetchedCustomerInfoMap[userWalletId] + } + + override suspend fun createOrder(userWalletId: UserWalletId) = withContext(dispatcherProvider.io) { launch { requestHelper.runWithErrorLogs(TAG) { - val walletAddress = requestHelper.getCustomerWalletAddress() - val result = requestHelper.request { authHeader -> + val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId) + val result = requestHelper.request(userWalletId) { authHeader -> tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) }.result ?: error("Create order result is null") @@ -141,7 +121,23 @@ internal class DefaultOnboardingRepository @Inject constructor( } } - private suspend fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo { + private fun getCardId(userWalletId: UserWalletId): String { + val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } + } else { + userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } + } ?: error("no userWallet found") + return if (userWallet is UserWallet.Cold) { + userWallet.cardId + } else { + TODO("[REDACTED_JIRA]") + } + } + + private suspend fun getCustomerInfo( + userWalletId: UserWalletId, + response: CustomerMeResponse.Result?, + ): CustomerInfo { val card = response?.card val balance = response?.balance val paymentAccount = response?.paymentAccount @@ -157,46 +153,59 @@ internal class DefaultOnboardingRepository @Inject constructor( null } val productInstance = response?.productInstance?.let { instance -> - cardFrozenStateStore.store( - key = instance.cardId, - value = when (instance.status) { - CustomerMeResponse.ProductInstance.Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen - else -> TangemPayCardFrozenState.Frozen - }, - ) - ProductInstance( - id = instance.id, - cardId = instance.cardId, - status = when (instance.status) { - CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE - else -> ProductInstance.Status.INACTIVE - }, - ) + val cardFrozenState = when (instance.status) { + CustomerMeResponse.ProductInstance.Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen + else -> TangemPayCardFrozenState.Frozen + } + cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) + + ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState) } return CustomerInfo( productInstance = productInstance, isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS, cardInfo = cardInfo, - ).also { lastFetchedCustomerInfo = it } - } - - private suspend fun getOrderStatus(orderId: String): OrderStatus { - val result = requestHelper.request { authHeader -> - tangemPayApi.getOrder(authHeader, orderId) - }.result ?: error("Order result is null") - - return when (result.status) { - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED + ).also { + lastFetchedCustomerInfoMap[userWalletId] = it } } - private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo { - val result = requestHelper.request { authHeader -> - tangemPayApi.getCustomerMe(authHeader) - }.result - return getCustomerInfo(result) + override suspend fun getOrderStatus( + userWalletId: UserWalletId, + orderId: String, + ): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) + }.map { response -> + when (response.result?.status) { + null -> OrderStatus.UNKNOWN + OrderStatus.NEW.apiName -> OrderStatus.NEW + OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING + OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED + else -> OrderStatus.CANCELED + } + } + } + + override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either { + val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) + if (hasTangemPay == true) { + return Either.Right(true) + } + + return requestHelper.performWithStaticToken { staticToken -> + tangemPayApi.checkCustomerWalletId( + authHeader = staticToken, + customerWalletId = userWalletId.stringValue, + ) + }.map { response -> + val id = response.id + if (!id.isNullOrEmpty()) { + tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId) + true + } else { + false + } + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 6fa164486c..b92ec16efe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -14,6 +14,7 @@ import com.tangem.datasource.api.pay.models.request.SetPinRequest import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails @@ -35,9 +36,9 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( private val cardFrozenStateStore: TangemPayCardFrozenStateStore, ) : TangemPayCardDetailsRepository { - override suspend fun getCardBalance(): Either { + override suspend fun getCardBalance(userWalletId: UserWalletId): Either { return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request { authHeader -> + val result = requestHelper.request(userWalletId) { authHeader -> tangemPayApi.getCardBalance(authHeader) }.result ?: error("Cannot get card balance") TangemPayCardBalance( @@ -47,12 +48,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } - override suspend fun revealCardDetails(): Either { + override suspend fun revealCardDetails(userWalletId: UserWalletId): Either { return requestHelper.runWithErrorLogs(TAG) { val publicKeyBase64 = getPublicKeyBase64() val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) - val result = requestHelper.request { authHeader -> + val result = requestHelper.request(userWalletId) { authHeader -> tangemPayApi.revealCardDetails( authHeader = authHeader, body = CardDetailsRequest(sessionId = sessionId), @@ -81,14 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } - override suspend fun setPin(pin: String): Either { + override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either { return requestHelper.runWithErrorLogs(TAG) { val publicKeyBase64 = getPublicKeyBase64() val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64) val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes) secretKeyBytes.fill(0) - val status = requestHelper.request { authHeader -> + val status = requestHelper.request(userWalletId) { authHeader -> tangemPayApi.setPin( authHeader = authHeader, body = SetPinRequest( @@ -107,21 +108,24 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } - override suspend fun isAddToWalletDone(): Either { + override suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either { return requestHelper.runWithErrorLogs(TAG) { - storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress()) + storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId)) } } - override suspend fun setAddToWalletAsDone(): Either { + override suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either { return requestHelper.runWithErrorLogs(TAG) { - storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(), isDone = true) + storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId), isDone = true) } } - override suspend fun freezeCard(cardId: String): Either { + override suspend fun freezeCard( + userWalletId: UserWalletId, + cardId: String, + ): Either { cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending) - return requestHelper.makeSafeRequest { + return requestHelper.makeSafeRequest(userWalletId) { tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId)) }.onLeft { cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen) @@ -141,9 +145,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } - override suspend fun unfreezeCard(cardId: String): Either { + override suspend fun unfreezeCard( + userWalletId: UserWalletId, + cardId: String, + ): Either { cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending) - return requestHelper.makeSafeRequest { + return requestHelper.makeSafeRequest(userWalletId) { tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId)) }.onLeft { cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen) 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 b7c1bac9d4..243e008c99 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 @@ -6,6 +6,7 @@ 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.models.wallet.UserWalletId import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig @@ -34,6 +35,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) } override fun getTxHistoryBatchFlow( + userWalletId: UserWalletId, batchSize: Int, context: TangemPayTxHistoryListBatchingContext, ): TangemPayTxHistoryListBatchFlow { @@ -41,18 +43,24 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( fetchDispatcher = dispatchers.io, context = context, generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 }, - batchFetcher = createFetcher(batchSize), + batchFetcher = createFetcher(userWalletId, batchSize), ).toBatchFlow() } private fun createFetcher( + userWalletId: UserWalletId, batchSize: Int, ): BatchFetcher> { return CursorBatchFetcher( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, _ -> - val items = loadItems(config = request.params, cursor = request.cursor, limit = request.limit) + val items = loadItems( + userWalletId = userWalletId, + config = request.params, + cursor = request.cursor, + limit = request.limit, + ) BatchFetchResult.Success( data = items, last = items.size < request.limit, @@ -64,6 +72,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( } private suspend fun loadItems( + userWalletId: UserWalletId, config: TangemPayTxHistoryListConfig, cursor: String?, limit: Int, @@ -71,7 +80,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( cacheRegistry.invokeOnExpire( key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor), skipCache = config.shouldRefresh, - block = { fetch(customerWalletAddress = config.customerWalletAddress, cursor = cursor, pageSize = limit) }, + block = { + fetch( + userWalletId = userWalletId, + customerWalletAddress = config.customerWalletAddress, + cursor = cursor, + pageSize = limit, + ) + }, ) return txHistoryItemsStore.getSyncOrNull( @@ -84,9 +100,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}" } - private suspend fun fetch(customerWalletAddress: String, cursor: String?, pageSize: Int) { + private suspend fun fetch( + userWalletId: UserWalletId, + customerWalletAddress: String, + cursor: String?, + pageSize: Int, + ) { requestPerformer.runWithErrorLogs(TAG) { - val result = requestPerformer.request { authHeader -> + val result = requestPerformer.request(userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.result val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() 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 32af571dc3..ecdc839b23 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 @@ -1,19 +1,20 @@ package com.tangem.data.pay.repository import arrow.core.Either -import arrow.core.Either.Companion.catch +import arrow.core.left +import arrow.core.raise.catch +import arrow.core.right import com.squareup.moshi.Moshi -import com.tangem.core.error.UniversalError +import com.squareup.wire.Instant import com.tangem.data.pay.util.TangemPayErrorConverter -import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* @@ -25,20 +26,21 @@ import javax.inject.Inject internal class TangemPayRequestPerformer @Inject constructor( @NetworkMoshi moshi: Moshi, + private val environmentConfigStorage: EnvironmentConfigStorage, private val dispatchers: CoroutineDispatcherProvider, private val tangemPayStorage: TangemPayStorage, private val authDataSource: TangemPayAuthDataSource, - private val tangemPayWalletsManager: TangemPayWalletsManager, ) { private val customerWalletAddress = MutableStateFlow(null) private val refreshTokensMutex = Mutex() - private var refreshTokensJob: Deferred? = null + private var refreshTokensJob: Deferred? = null private val errorConverter = TangemPayErrorConverter(moshi) - suspend fun runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either { + @Deprecated("Do not use this method") + suspend fun runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either { return try { val result = requestBlock() Either.Right(result) @@ -56,54 +58,81 @@ internal class TangemPayRequestPerformer @Inject constructor( } suspend fun makeSafeRequest( + userWalletId: UserWalletId, requestBlock: suspend (header: String) -> ApiResponse, ): Either { - return catch { request(requestBlock) } - .mapLeft { exception -> - Timber.tag("TangemPayRequestPerformer").e(exception) - errorConverter.convert(exception) - } + return performRequest(userWalletId, requestBlock) } - suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): T = - withContext(dispatchers.io) { - performRequest( - requestBlock = requestBlock, - getTokens = ::getAccessTokens, - refreshTokens = ::refreshAuthTokens, + @Deprecated("Use perform request instead", replaceWith = ReplaceWith("performRequest")) + suspend fun request( + userWalletId: UserWalletId, + requestBlock: suspend (header: String) -> + ApiResponse, + ): T = withContext(dispatchers.io) { + performRequest(userWalletId, requestBlock = requestBlock) + // to keep behaviour as previous + .fold( + ifRight = { it }, + ifLeft = { error -> error("Cannot perform request: $error") }, ) - } - - private suspend fun performRequest( - requestBlock: suspend (header: String) -> ApiResponse, - getTokens: (suspend () -> VisaAuthTokens), - refreshTokens: (suspend () -> VisaAuthTokens)? = null, - ): T = runCatching { - 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) { - refreshOrJoin(refreshTokens) - performRequest(requestBlock, refreshTokens = null, getTokens = getTokens) - } else { - throw error - } } - private suspend fun refreshOrJoin(refreshTokens: suspend () -> VisaAuthTokens): VisaAuthTokens { - val jobToAwait: Deferred = - refreshTokensMutex.withLock { - val current = refreshTokensJob - if (current == null || current.isCompleted) { - coroutineScope { - async { refreshTokens() }.also { refreshTokensJob = it } - } - } else { - current + suspend fun performWithStaticToken( + requestBlock: suspend (header: String) -> ApiResponse, + ): Either = withContext(dispatchers.io) { + catch( + block = { + val staticToken = + environmentConfigStorage.getConfigSync().bffStaticToken ?: error("BFF static token is null") + when (val apiResponse = requestBlock(staticToken)) { + is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left() + is ApiResponse.Success -> apiResponse.data.right() } + }, + catch = { errorConverter.convert(it).left() }, + ) + } + + suspend fun performRequest( + userWalletId: UserWalletId, + requestBlock: suspend (header: String) -> ApiResponse, + ): Either = withContext(dispatchers.io) { + catch( + block = { + val tokens = getAccessTokens(userWalletId) + val now = Instant.now() + val accessExpiresAt = Instant.ofEpochSecond(tokens.expiresAt) + val refreshExpiresAt = Instant.ofEpochSecond(tokens.refreshExpiresAt) + val apiResponse: ApiResponse = if (accessExpiresAt.isAfter(now)) { + requestBlock(tokens.getAuthHeader()) + } else if (accessExpiresAt.isBefore(now) && refreshExpiresAt.isAfter(now)) { + val newTokens = refreshOrJoin(refreshTokens = { refreshAuthTokens(userWalletId) }) + requestBlock(newTokens.getAuthHeader()) + } else { + return@catch VisaApiError.RefreshTokenExpired.left() + } + + when (apiResponse) { + is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left() + is ApiResponse.Success -> apiResponse.data.right() + } + }, + catch = { errorConverter.convert(it).left() }, + ).onLeft { visaApiError -> Timber.tag("TangemPayRequestPerformer").e(visaApiError.toString()) } + } + + private suspend fun refreshOrJoin(refreshTokens: suspend () -> TangemPayAuthTokens): TangemPayAuthTokens { + val jobToAwait: Deferred = refreshTokensMutex.withLock { + val current = refreshTokensJob + if (current == null || current.isCompleted) { + coroutineScope { + async { refreshTokens() }.also { refreshTokensJob = it } + } + } else { + current } + } val result = try { jobToAwait.await() } finally { @@ -116,28 +145,28 @@ internal class TangemPayRequestPerformer @Inject constructor( return result } - suspend fun getCustomerWalletAddress(): String { + suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String { val existingAddress = customerWalletAddress.value if (existingAddress != null) { return existingAddress } val storedAddress = tangemPayStorage.getCustomerWalletAddress( - userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId, + userWalletId = userWalletId, ) ?: error("Can not find customer address") customerWalletAddress.value = storedAddress return storedAddress } - private suspend fun getAccessTokens(): VisaAuthTokens { - val walletAddress = getCustomerWalletAddress() + private suspend fun getAccessTokens(userWalletId: UserWalletId): TangemPayAuthTokens { + val walletAddress = getCustomerWalletAddress(userWalletId) 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 + private suspend fun refreshAuthTokens(userWalletId: UserWalletId): TangemPayAuthTokens { + val customerWalletAddress = getCustomerWalletAddress(userWalletId) + val refreshToken = getAccessTokens(userWalletId).refreshToken val tokens = authDataSource.refreshAuthTokens(refreshToken) .fold( ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index 7cf0044f76..d5fa97ece5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -12,6 +12,8 @@ class TangemPayErrorConverter(moshi: Moshi) : Converter override fun convert(value: Throwable): VisaApiError { return if (value is ApiResponseError.HttpException) { + if (value.code == ApiResponseError.HttpException.Code.NOT_FOUND) return VisaApiError.NotPaeraCustomer + val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode return runCatching { visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt index 32c775d61a..b4c31a08cb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt @@ -8,7 +8,8 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import javax.inject.Inject -internal class TangemPayWalletsManager @Inject constructor( +// TODO remove after implement wallet selector in pay +class TangemPayWalletsManager @Inject constructor( private val manager: UserWalletsListManager, private val repository: UserWalletsListRepository, private val hotWalletFeatureToggles: HotWalletFeatureToggles, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt index baadf36595..78d5cc47eb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt @@ -5,15 +5,13 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.TangemPayAuthApi import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi -import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.VisaAuthChallenge -import com.tangem.domain.visa.model.VisaAuthSession -import com.tangem.domain.visa.model.VisaAuthSignedChallenge -import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject @@ -21,6 +19,7 @@ import javax.inject.Inject internal class DefaultVisaAuthRemoteDataSource @Inject constructor( @NetworkMoshi private val moshi: Moshi, private val visaAuthApi: TangemPayApi, + private val tangemPayAuthApi: TangemPayAuthApi, private val dispatchers: CoroutineDispatcherProvider, ) : VisaAuthRemoteDataSource { @@ -66,15 +65,19 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( override suspend fun getCustomerWalletAuthChallenge( customerWalletAddress: String, + customerWalletId: String, ): Either = withContext(dispatchers.io) { request { - visaAuthApi.generateNonceByCustomerWallet( - GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress), + tangemPayAuthApi.generateNonceByCustomerWallet( + request = GenerateNonceByCustomerWalletRequest( + customerWalletAddress = customerWalletAddress, + customerWalletId = customerWalletId, + ), ).getOrThrow() }.map { response -> VisaAuthChallenge.Wallet( - challenge = response.result.nonce, - session = VisaAuthSession(response.result.sessionId), + challenge = response.nonce, + session = VisaAuthSession(response.sessionId), ) } } @@ -83,37 +86,42 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( sessionId: String, signature: String, nonce: String, - ): Either = withContext(dispatchers.io) { + ): Either = withContext(dispatchers.io) { request { - visaAuthApi.getTokenByCustomerWallet( - GetTokenByCustomerWalletRequest( + tangemPayAuthApi.getTokenByCustomerWallet( + request = GetTokenByCustomerWalletRequest( + authType = "customer_wallet", sessionId = sessionId, signature = signature, messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", ), ).getOrThrow() }.map { response -> - VisaAuthTokens( - response.result.accessToken, - VisaAuthTokens.RefreshToken( - response.result.refreshToken, - VisaAuthTokens.RefreshToken.Type.CardWallet, - ), + TangemPayAuthTokens( + accessToken = response.accessToken, + expiresAt = response.expiresAt, + refreshToken = response.refreshToken, + refreshExpiresAt = response.refreshExpiresAt, ) } } override suspend fun refreshCustomerWalletAuthTokens( - refreshToken: VisaAuthTokens.RefreshToken, - ): Either = withContext(dispatchers.io) { + refreshToken: String, + ): Either = withContext(dispatchers.io) { request { - visaAuthApi.refreshCustomerWalletAccessToken( - RefreshCustomerWalletAccessTokenRequest(refreshToken = refreshToken.value), + tangemPayAuthApi.refreshCustomerWalletAccessToken( + request = RefreshCustomerWalletAccessTokenRequest( + authType = "customer_wallet", + refreshToken = refreshToken, + ), ).getOrThrow() }.map { response -> - VisaAuthTokens( - accessToken = response.result.accessToken, - refreshToken = refreshToken.copy(value = response.result.refreshToken), + TangemPayAuthTokens( + accessToken = response.accessToken, + expiresAt = response.expiresAt, + refreshToken = response.refreshToken, + refreshExpiresAt = response.refreshExpiresAt, ) } } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index e3bc73d03b..22faf074b1 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(deps.spongecastle.core) /** Libs - Other */ + implementation(deps.timber) implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) implementation(deps.moshi) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index ec42fc56c5..c94c767e02 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -61,6 +61,7 @@ sealed class VisaApiError( fun isUnknown() = this is UnknownWithoutCode || this is Unknown data object RefreshTokenExpired : VisaApiError(104004001) + data object NotPaeraCustomer : VisaApiError(104004002) companion object { fun fromBackendError(backendErrorCode: Int): VisaApiError { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt new file mode 100644 index 0000000000..eb704a588a --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayAuthTokens.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.visa.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class TangemPayAuthTokens( + @Json(name = "access_token") val accessToken: String, + @Json(name = "expires_at") val expiresAt: Long, + @Json(name = "refresh_token") val refreshToken: String, + @Json(name = "refresh_expires_at") val refreshExpiresAt: Long, +) + +fun TangemPayAuthTokens.getAuthHeader(): String { + return "Bearer $accessToken" +} \ No newline at end of file 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 index 40a1132513..28fc980690 100644 --- 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 @@ -1,3 +1,3 @@ package com.tangem.domain.visa.model -data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens) \ No newline at end of file +data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: TangemPayAuthTokens) \ 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 69ec635f4a..4b6c93492d 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,12 +1,12 @@ package com.tangem.domain.pay.datasource import arrow.core.Either +import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.TangemPayInitialCredentials -import com.tangem.domain.visa.model.VisaAuthTokens interface TangemPayAuthDataSource { suspend fun produceInitialCredentials(cardId: String): Either - suspend fun refreshAuthTokens(refreshToken: String): Either + suspend fun refreshAuthTokens(refreshToken: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 455e70bd12..7f1092646f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -1,5 +1,6 @@ package com.tangem.domain.pay.model +import com.tangem.domain.visa.model.TangemPayCardFrozenState import java.math.BigDecimal data class MainScreenCustomerInfo( @@ -16,13 +17,8 @@ data class CustomerInfo( data class ProductInstance( val id: String, val cardId: String, - val status: Status, - ) { - enum class Status { - ACTIVE, - INACTIVE, - } - } + val cardFrozenState: TangemPayCardFrozenState, + ) data class CardInfo( val lastFourDigits: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt new file mode 100644 index 0000000000..7fda6cfe51 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.model + +sealed interface TangemPayCustomerInfoError { + + data object UnavailableError : TangemPayCustomerInfoError + data object RefreshNeededError : TangemPayCustomerInfoError + data object UnknownError : TangemPayCustomerInfoError +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index 01d3a268d7..3abf5c2b0e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo interface KycRepository { @@ -9,5 +10,5 @@ interface KycRepository { /** * Returns KYC data to start or continue the survey */ - suspend fun getKycStartInfo(): Either + suspend fun getKycStartInfo(userWalletId: UserWalletId): Either } \ 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 ba7232b18d..0010b99546 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 @@ -2,23 +2,27 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.CustomerInfo -import com.tangem.domain.pay.model.MainScreenCustomerInfo +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.visa.error.VisaApiError +import kotlinx.coroutines.Job interface OnboardingRepository { suspend fun validateDeeplink(link: String): Either - suspend fun isTangemPayInitialDataProduced(): Boolean + suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean - suspend fun produceInitialData() + suspend fun produceInitialData(userWalletId: UserWalletId) - suspend fun getCustomerInfo(): Either + suspend fun getCustomerInfo(userWalletId: UserWalletId): Either - /** - * Returns only if the user already authorised at least once - */ - suspend fun getMainScreenCustomerInfo(): Either + suspend fun createOrder(userWalletId: UserWalletId): Job - fun getSavedCustomerInfo(): CustomerInfo? + suspend fun clearOrderId(userWalletId: UserWalletId) + suspend fun getOrderId(userWalletId: UserWalletId): String? + suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either + suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 6ce352de48..73a4b8e035 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails @@ -10,18 +11,22 @@ import kotlinx.coroutines.flow.Flow interface TangemPayCardDetailsRepository { - suspend fun getCardBalance(): Either + suspend fun getCardBalance(userWalletId: UserWalletId): Either - suspend fun revealCardDetails(): Either + suspend fun revealCardDetails(userWalletId: UserWalletId): Either - suspend fun setPin(pin: String): Either + suspend fun setPin(userWalletId: UserWalletId, pin: String): Either - suspend fun isAddToWalletDone(): Either + suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either - suspend fun setAddToWalletAsDone(): Either + suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either + + suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either + suspend fun unfreezeCard( + userWalletId: UserWalletId, + cardId: String, + ): Either - suspend fun freezeCard(cardId: String): Either - suspend fun unfreezeCard(cardId: String): Either fun cardFrozenState(cardId: String): Flow suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? } \ No newline at end of file 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 bddeb01eaf..68ca205647 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 @@ -2,20 +2,16 @@ package com.tangem.domain.pay.usecase import arrow.core.Either import arrow.core.Either.Companion.catch +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.repository.OnboardingRepository class ProduceTangemPayInitialDataUseCase( private val repository: OnboardingRepository, ) { - suspend operator fun invoke(): Either { + suspend operator fun invoke(userWalletId: UserWalletId): Either { return catch { - val isDataProduced = repository.isTangemPayInitialDataProduced() - if (isDataProduced) { - return@catch Unit - } else { - repository.produceInitialData() - } + repository.produceInitialData(userWalletId) } } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 1f49a481e2..1d3d10d652 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -1,7 +1,19 @@ package com.tangem.domain.pay.usecase +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.catch +import arrow.core.right +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.visa.error.VisaApiError +import timber.log.Timber + +private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" /** * Returns tangem pay customer info for the main screen banner @@ -11,5 +23,96 @@ class TangemPayMainScreenCustomerInfoUseCase( private val repository: OnboardingRepository, ) { - suspend operator fun invoke(): MainScreenCustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull() + suspend operator fun invoke( + userWalletId: UserWalletId, + ): Either = catch( + block = { + repository.checkCustomerWallet(userWalletId) + .fold( + ifLeft = { TangemPayCustomerInfoError.UnknownError.left() }, + ifRight = { hasTangemPay -> + if (hasTangemPay) { + proceedWithPaeraCustomerResult(userWalletId) + } else { + TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay + } + }, + ) + }, + catch = { error -> + Timber.tag(TAG).e(error) + TangemPayCustomerInfoError.UnknownError.left() + }, + ) + + private suspend fun proceedWithPaeraCustomerResult( + userWalletId: UserWalletId, + ): Either { + val orderId = repository.getOrderId(userWalletId) + return if (orderId != null) { + proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + } else { + proceedWithoutOrder(userWalletId = userWalletId) + } + } + + private suspend fun proceedWithoutOrder( + userWalletId: UserWalletId, + ): Either { + return repository.getCustomerInfo(userWalletId) + .mapLeft { error -> error.mapErrorForCustomer() } + .map { customerInfo -> + if (customerInfo.cardInfo == null) { + // If order id wasn't saved -> start order creation and get customer info + repository.createOrder(userWalletId) + } + MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN) + } + } + + private suspend fun proceedWithOrderId( + userWalletId: UserWalletId, + orderId: String, + ): Either { + return repository.getOrderStatus(userWalletId, orderId = orderId) + .fold( + ifLeft = { error -> + error.mapErrorForCustomer().left() + }, + ifRight = { orderStatus -> + when (orderStatus) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> MainScreenCustomerInfo( + info = CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null), + orderStatus = orderStatus, + ).right() + + // Order was created/cancelled -> clear order id and get customer info + OrderStatus.COMPLETED, + OrderStatus.CANCELED, + OrderStatus.UNKNOWN, + -> { + repository.clearOrderId(userWalletId) + // If order was cancelled -> start order creation + if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId) + repository.getCustomerInfo(userWalletId = userWalletId) + .mapLeft { it.mapErrorForCustomer() } + .map { customerInfo -> + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + } + } + } + }, + ) + } + + private fun VisaApiError.mapErrorForCustomer(): TangemPayCustomerInfoError { + return if (this !is VisaApiError.NotPaeraCustomer) { + TangemPayCustomerInfoError.UnavailableError + } else { + TangemPayCustomerInfoError.UnknownError + } + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt index 7915f1b83d..50bf30ba9e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt @@ -1,10 +1,12 @@ package com.tangem.domain.tangempay.repository +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext interface TangemPayTxHistoryRepository { fun getTxHistoryBatchFlow( + userWalletId: UserWalletId, batchSize: Int, context: TangemPayTxHistoryListBatchingContext, ): TangemPayTxHistoryListBatchFlow diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt index 251532eeea..835c2b07a4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt @@ -2,6 +2,7 @@ package com.tangem.domain.visa.datasource import arrow.core.Either import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSignedChallenge import com.tangem.domain.visa.model.VisaAuthTokens @@ -20,17 +21,16 @@ interface VisaAuthRemoteDataSource { suspend fun getCustomerWalletAuthChallenge( customerWalletAddress: String, + customerWalletId: String, ): Either suspend fun getTokenWithCustomerWallet( sessionId: String, signature: String, nonce: String, - ): Either + ): Either - suspend fun refreshCustomerWalletAuthTokens( - refreshToken: VisaAuthTokens.RefreshToken, - ): Either + suspend fun refreshCustomerWalletAuthTokens(refreshToken: String): Either suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt index 97e2d1f0cc..dcae30ff3b 100644 --- a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -2,10 +2,11 @@ package com.tangem.features.kyc import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId interface KycComponent : ComposableContentComponent { - data object Params + data class Params(val userWalletId: UserWalletId) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt index 7df782304f..6b44954fab 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -3,27 +3,36 @@ package com.tangem.features.kyc 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.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @ModelScoped class DefaultKycModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val kycRepository: KycRepository, ) : Model() { + private val params: KycComponent.Params = paramsContainer.require() + private val _uiState: MutableStateFlow = MutableStateFlow(null) val uiState = _uiState.asStateFlow() init { modelScope.launch { - kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) } + try { + kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) } + } catch (e: Exception) { + Timber.e(e) + } } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index b1f3fb02be..126f7124d8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -63,6 +63,7 @@ class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor( ) TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent( appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = params, ) TangemPayDetailsInnerRoute.ChangePINSuccess -> TangemPayChangePinSuccessComponent( appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayChangePinComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayChangePinComponent.kt index 35ae97b213..7b3ecc0c87 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayChangePinComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayChangePinComponent.kt @@ -14,9 +14,10 @@ import com.tangem.features.tangempay.ui.TangemPayChangePinScreen internal class TangemPayChangePinComponent( private val appComponentContext: AppComponentContext, + params: TangemPayDetailsContainerComponent.Params, ) : AppComponentContext by appComponentContext, ComposableContentComponent { - private val model: TangemPayChangePinModel = getOrCreateModel() + private val model: TangemPayChangePinModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 6f3d6490e8..38d29ced5d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -41,6 +41,7 @@ internal class TangemPayDetailsComponent( private val txHistoryComponent = DefaultTangemPayTxHistoryComponent( appComponentContext = child("txHistoryComponent"), params = DefaultTangemPayTxHistoryComponent.Params( + userWalletId = params.userWalletId, customerWalletAddress = params.config.customerWalletAddress, uiActions = model, ), 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 42a85d03c4..bfd618dfa5 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 @@ -4,6 +4,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM import com.tangem.features.tangempay.model.TangemPayTxHistoryModel import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems @@ -22,5 +23,9 @@ internal class DefaultTangemPayTxHistoryComponent( tangemPayTxHistoryItems(listState, state) } - data class Params(val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions) + data class Params( + val userWalletId: UserWalletId, + 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/model/TangemPayCardDetailsBlockModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt index 395b68dd3b..5c83a87360 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt @@ -78,7 +78,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( uiState.transformerUpdate( transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails), ) - cardDetailsRepository.revealCardDetails() + cardDetailsRepository.revealCardDetails(params.params.userWalletId) .onRight { cardDetails -> uiState.transformerUpdate( transformer = DetailsRevealedStateTransformer( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt index 826d5f4c0a..405a45ad13 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt @@ -3,28 +3,31 @@ 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.decompose.navigation.Router import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.entity.TangemPayChangePinUM -import kotlinx.coroutines.flow.* import com.tangem.features.tangempay.model.transformers.PinCodeChangeTransformer import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @ModelScoped internal class TangemPayChangePinModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val cardDetailsRepository: TangemPayCardDetailsRepository, ) : Model() { + private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(getInitialState()) @@ -35,7 +38,15 @@ internal class TangemPayChangePinModel @Inject constructor( private fun onClickSubmit() { modelScope.launch { uiState.update { it.copy(submitButtonLoading = true) } - val result = cardDetailsRepository.setPin(uiState.value.pinCode).getOrNull() + val result = try { + cardDetailsRepository.setPin( + userWalletId = params.userWalletId, + pin = uiState.value.pinCode, + ).getOrNull() + } catch (e: Exception) { + Timber.e(e) + return@launch + } uiState.update { it.copy(submitButtonLoading = false) } when (result) { SetPinResult.SUCCESS -> router.push(TangemPayDetailsInnerRoute.ChangePINSuccess) 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 675f2e3db5..1f9565d831 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 @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -123,7 +124,13 @@ internal class TangemPayDetailsModel @Inject constructor( private fun freezeCard() { modelScope.launch { - cardDetailsRepository.freezeCard(cardId = params.config.cardId) + val result = try { + cardDetailsRepository.freezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) + } catch (e: Exception) { + Timber.e(e) + return@launch + } + result .onLeft { uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed))) } @@ -155,7 +162,13 @@ internal class TangemPayDetailsModel @Inject constructor( private fun unfreezeCard() { modelScope.launch { - cardDetailsRepository.unfreezeCard(cardId = params.config.cardId) + val result = try { + cardDetailsRepository.unfreezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) + } catch (e: Exception) { + Timber.e(e) + return@launch + } + result .onLeft { uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed))) } @@ -204,7 +217,12 @@ internal class TangemPayDetailsModel @Inject constructor( private fun fetchBalance(): Job { return modelScope.launch { - val result = cardDetailsRepository.getCardBalance().onRight { balance = it } + val result = try { + cardDetailsRepository.getCardBalance(params.userWalletId).onRight { balance = it } + } catch (e: Exception) { + Timber.e(e) + return@launch + } uiState.update(DetailsBalanceTransformer(balance = result)) }.saveIn(fetchBalanceJobHolder) } @@ -217,7 +235,12 @@ internal class TangemPayDetailsModel @Inject constructor( private fun fetchAddToWalletBanner() { modelScope.launch { - val isDone = cardDetailsRepository.isAddToWalletDone().getOrNull() ?: false + val isDone = try { + cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true + } catch (e: Exception) { + Timber.e(e) + return@launch + } uiState.update( transformer = DetailsAddToWalletBannerTransformer( onClickBanner = ::onClickAddToWalletBlock, @@ -244,7 +267,11 @@ internal class TangemPayDetailsModel @Inject constructor( private fun onClickCloseAddToWalletBlock() { modelScope.launch { - cardDetailsRepository.setAddToWalletAsDone() + try { + cardDetailsRepository.setAddToWalletAsDone(params.userWalletId) + } catch (e: Exception) { + Timber.e(e) + } uiState.update( transformer = DetailsAddToWalletBannerTransformer( onClickBanner = ::onClickAddToWalletBlock, 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 de78655d29..e1433f4d17 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 @@ -46,7 +46,7 @@ internal class TangemPayTxHistoryModel @Inject constructor( } private fun launchPagination() { - modelScope.launch { listManager.launchPagination() } + modelScope.launch { listManager.launchPagination(params.userWalletId) } } private fun subscribeToUiItemChanges() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsHiddenStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsHiddenStateTransformer.kt index 867751f14a..0856ea376b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsHiddenStateTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsHiddenStateTransformer.kt @@ -9,6 +9,6 @@ internal class DetailsHiddenStateTransformer( ) : Transformer { override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM { - return stateFactory.getInitialState() + return stateFactory.getInitialState().copy(cardFrozenState = prevState.cardFrozenState) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index 40cd31c7da..f1b971367c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -1,5 +1,6 @@ package com.tangem.features.tangempay.utils +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository @@ -36,8 +37,9 @@ internal class TangemPayTxHistoryListManager( val emptyStatus: Flow = state.map { it.isEmpty }.distinctUntilChanged() val paginationStatus: Flow> = state.map { it.status }.distinctUntilChanged() - suspend fun launchPagination() = coroutineScope { + suspend fun launchPagination(userWalletId: UserWalletId) = coroutineScope { val batchFlow = repository.getTxHistoryBatchFlow( + userWalletId = userWalletId, context = TangemPayTxHistoryListBatchingContext(actionsFlow = actionsFlow, coroutineScope = this), batchSize = 50, ) diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 7b91afb8fd..a35a3e4657 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -32,6 +32,9 @@ dependencies { /** Domain */ implementation(projects.domain.visa) + /** Data **/ + implementation(projects.data.visa) + /** Compose */ implementation(deps.compose.foundation) implementation(deps.compose.material3) 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 a102748888..688336b8ca 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 @@ -6,10 +6,11 @@ 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.decompose.navigation.Router -import com.tangem.features.tangempay.TangemPayConstants import com.tangem.core.navigation.url.UrlOpener +import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase +import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState @@ -17,13 +18,14 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import com.tangem.utils.transformer.update as transformerUpdate import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import com.tangem.utils.transformer.update as transformerUpdate @Stable @ModelScoped +@Suppress("LongParameterList") internal class TangemPayOnboardingModel @Inject constructor( paramsContainer: ParamsContainer, private val router: Router, @@ -31,6 +33,7 @@ internal class TangemPayOnboardingModel @Inject constructor( private val repository: OnboardingRepository, private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, private val urlOpener: UrlOpener, + private val tangemPayWalletsManager: TangemPayWalletsManager, ) : Model() { private val params = paramsContainer.require() @@ -66,7 +69,11 @@ internal class TangemPayOnboardingModel @Inject constructor( } private suspend fun checkCustomerInfo() { - repository.getCustomerInfo() + // TODO implement selector + repository.getCustomerInfo( + userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId, + ) + // selector .onRight { customerInfo -> when { !customerInfo.isKycApproved -> { @@ -88,32 +95,42 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun onGetCardClick() { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) modelScope.launch { - val result = produceInitialDataUseCase() + // TODO implement selector + val userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId + val result = produceInitialDataUseCase(userWalletId) if (result.isLeft()) { Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } - repository.getCustomerInfo() - .fold( - ifLeft = { - Timber.e("Error getCustomerInfo: ${it.errorCode}") - uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) - }, - ifRight = { customerInfo -> - if (customerInfo.isKycApproved) { - back() - } else { - openKyc() - } - }, - ) + // TODO implement selector + repository.getCustomerInfo( + userWalletId = userWalletId, + ).fold( + ifLeft = { + Timber.e("Error getCustomerInfo: ${it.errorCode}") + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) + }, + ifRight = { customerInfo -> + if (customerInfo.isKycApproved) { + back() + } else { + openKyc() + } + }, + ) } } private fun openKyc() { - router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) + // TODO implement selector + router.replaceAll( + AppRoute.Wallet, + AppRoute.Kyc( + userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId, + ), + ) } private fun back() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/TangemPayMainInfoManager.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/TangemPayMainInfoManager.kt new file mode 100644 index 0000000000..184cc926bd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/TangemPayMainInfoManager.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.child.wallet.model + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.MainScreenCustomerInfo +import com.tangem.domain.pay.model.TangemPayCustomerInfoError +import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class TangemPayMainInfoManager @Inject constructor( + private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, +) { + + val mainScreenCustomerInfo: + StateFlow>?> + field = MutableStateFlow(null) + + suspend fun refreshTangemPayInfo(userWalletId: UserWalletId) { + val info = tangemPayMainScreenCustomerInfoUseCase(userWalletId) + mainScreenCustomerInfo.value = Pair(userWalletId, info) + } +} \ No newline at end of file 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 385eed899a..2746942333 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 @@ -20,10 +20,9 @@ import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.pay.model.MainScreenCustomerInfo -import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository -import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.visa.model.TangemPayCardFrozenState @@ -94,13 +93,13 @@ internal class WalletModel @Inject constructor( private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, - private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val tangemPayMainInfoManager: TangemPayMainInfoManager, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -137,7 +136,8 @@ internal class WalletModel @Inject constructor( subscribeOnSelectedWalletFlow() subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() - subscribeToTangemPayInfo() + subscribeTangemPayOnWalletState() + subscribeOnTangemPayInfoUpdates() enableNotificationsIfNeeded() clickIntents.initialize(innerWalletRouter, modelScope) @@ -349,57 +349,86 @@ internal class WalletModel @Inject constructor( .saveIn(clearNFTCacheJobHolder) } - private fun subscribeToTangemPayInfo() { + private fun subscribeTangemPayOnWalletState() { /** * Update state each time a user opens/returns to wallet screen * and every minute while user stays on the main screen */ - screenLifecycleProvider.isBackgroundState.onEach { inBackground -> - // fast exit - if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach + if (!tangemPayFeatureToggles.isTangemPayEnabled) return - // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh - val savedCustomerInfo = tangemPayOnboardingRepository.getSavedCustomerInfo() - if (savedCustomerInfo?.cardInfo != null) { - updateTangemPay(MainScreenCustomerInfo(info = savedCustomerInfo, orderStatus = OrderStatus.COMPLETED)) - return@onEach - } + combine( + flow = screenLifecycleProvider.isBackgroundState, + flow2 = uiState.mapNotNull { + it.wallets.getOrNull(it.selectedWalletIndex)?.walletCardState?.id + }.distinctUntilChanged(), + transform = ::Pair, + ).onEach { (inBackground, userWalletId) -> + val savedCustomerInfo = + tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId) - updateTangemPayJobHolder.cancel() + val isShouldLaunchPeriodicUpdate = savedCustomerInfo?.cardInfo == null && + tangemPayOnboardingRepository.isTangemPayInitialDataProduced(userWalletId) - modelScope.launch { - // fast exit - val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced() - if (!initialDataProduced) return@launch - - if (!inBackground) { - refreshTangemPayInfo() - while (isActive) { - delay(TANGEM_PAY_UPDATE_INTERVAL) - refreshTangemPayInfo() + if (isShouldLaunchPeriodicUpdate) { + updateTangemPayJobHolder.cancel() + modelScope.launch { + if (!inBackground) { + tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) + while (isActive) { + delay(TANGEM_PAY_UPDATE_INTERVAL) + tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) + } } - } - }.saveIn(updateTangemPayJobHolder) + }.saveIn(updateTangemPayJobHolder) + } else { + // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh + tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) + } }.launchIn(modelScope) } - private suspend fun refreshTangemPayInfo() { - val info = tangemPayMainScreenCustomerInfoUseCase() - if (info != null) updateTangemPay(info) + private fun subscribeOnTangemPayInfoUpdates() { + if (!tangemPayFeatureToggles.isTangemPayEnabled) return + tangemPayMainInfoManager.mainScreenCustomerInfo + .filterNotNull() + .distinctUntilChanged() + .onEach { (userWalletId, mainInfoData) -> + mainInfoData.onLeft { tangemPayError -> + when (tangemPayError) { + TangemPayCustomerInfoError.RefreshNeededError -> { + stateHolder.update( + transformer = TangemPayRefreshNeededStateTransformer( + onRefreshClick = { clickIntents.onRefreshPayToken(userWalletId) }, + ), + ) + } + TangemPayCustomerInfoError.UnavailableError -> { + stateHolder.update( + transformer = TangemPayUnavailableStateTransformer, + ) + } + else -> { + // do not draw TangemPay block + Timber.e("Failed when loading main screen TangemPay info: $tangemPayError") + } + } + }.onRight { data -> updateTangemPay(data, userWalletId) } + } + .launchIn(modelScope) } - private suspend fun updateTangemPay(info: MainScreenCustomerInfo) { + private suspend fun updateTangemPay(data: MainScreenCustomerInfo, userWalletId: UserWalletId) { val cardFrozenState = - info.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) } + data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) } ?: TangemPayCardFrozenState.Unfrozen stateHolder.update( transformer = TangemPayInitialStateTransformer( - value = info, + value = data, cardFrozenState = cardFrozenState, onClickKyc = innerWalletRouter::openTangemPayOnboarding, openDetails = { config -> innerWalletRouter.openTangemPayDetails( - userWalletId = stateHolder.getSelectedWalletId(), + userWalletId = userWalletId, config = config, ) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index f770c20ad2..4f13aa6654 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -1,45 +1,45 @@ package com.tangem.feature.wallet.child.wallet.model.intents import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.repository.OnboardingRepository -import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository -import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase -import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase +import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayInitialStateTransformer import com.tangem.features.tangempay.TangemPayFeatureToggles +import kotlinx.coroutines.launch import javax.inject.Inject internal interface TangemPayIntents { suspend fun onPullToRefresh() + + fun onRefreshPayToken(userWalletId: UserWalletId) } @ModelScoped internal class TangemPayClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, - private val mainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val featureToggles: TangemPayFeatureToggles, private val onboardingRepository: OnboardingRepository, - private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase, + private val tangemPayInfoManager: TangemPayMainInfoManager, ) : BaseWalletClickIntents(), TangemPayIntents { override suspend fun onPullToRefresh() { - if (!featureToggles.isTangemPayEnabled || !onboardingRepository.isTangemPayInitialDataProduced()) return - val info = mainScreenCustomerInfoUseCase() val userWalletId = stateHolder.getSelectedWalletId() - if (info != null) { - val cardFrozenState = - info.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) } - ?: TangemPayCardFrozenState.Unfrozen - stateHolder.update( - transformer = TangemPayInitialStateTransformer( - value = info, - onClickKyc = router::openTangemPayOnboarding, - openDetails = { config -> router.openTangemPayDetails(userWalletId, config) }, - cardFrozenState = cardFrozenState, - ), - ) + if (!featureToggles.isTangemPayEnabled || + !onboardingRepository.isTangemPayInitialDataProduced(userWalletId) + ) { + return + } + tangemPayInfoManager.refreshTangemPayInfo(userWalletId) + } + + override fun onRefreshPayToken(userWalletId: UserWalletId) { + modelScope.launch { + produceInitialDataTangemPay.invoke(userWalletId) + tangemPayInfoManager.refreshTangemPayInfo(userWalletId) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 1f136f0dc3..ab979acec8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -4,8 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent -import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program +import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -50,16 +49,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError is WalletNotification.NoteMigration -> MainScreen.NotePromo - is WalletNotification.SwapPromo -> PromoAnalyticsEvent.NoticePromotionBanner( + is WalletNotification.SwapPromo -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Empty, // Use it on new promo action ) - is WalletNotification.Sepa -> PromoAnalyticsEvent.NoticePromotionBanner( + is WalletNotification.Sepa -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Sepa, ) is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo - is WalletNotification.VisaPresalePromo -> PromoAnalyticsEvent.VisaWaitlistPromo + is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, @@ -73,6 +72,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner + is WalletNotification.Warning.TangemPayRefreshNeeded -> null + WalletNotification.Warning.TangemPayUnreachable -> null } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt index aceb99de7b..7159878276 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -23,4 +23,10 @@ internal sealed class TangemPayState { val balanceText: TextReference, val onClick: () -> Unit, ) : TangemPayState() + + data class RefreshNeeded( + val notification: WalletNotification, + ) : TangemPayState() + + data class TemporaryUnavailable(val notification: WalletNotification) : TangemPayState() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 62e29d2cac..638da3f2e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -143,6 +143,24 @@ sealed class WalletNotification(val config: NotificationConfig) { title = resourceReference(R.string.yield_module_main_view_approve_notification_title), subtitle = resourceReference(R.string.yield_module_main_view_approve_notification_description), ) + + data object TangemPayUnreachable : Warning( + title = resourceReference(id = R.string.tangempay_temporarily_unavailable), + subtitle = resourceReference(id = R.string.tangempay_service_unreachable_try_later), + ) + + data class TangemPayRefreshNeeded( + @DrawableRes val tangemIcon: Int?, + val onRefreshClick: () -> Unit, + ) : Warning( + title = resourceReference(id = R.string.tangempay_payment_account_sync_needed), + subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), + buttonsState = ButtonsState.PrimaryButtonConfig( + text = resourceReference(id = R.string.home_button_scan), + iconResId = tangemIcon, + onClick = onRefreshClick, + ), + ) } sealed class Informational( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt new file mode 100644 index 0000000000..5e6a8cbb52 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState + +internal class TangemPayRefreshNeededStateTransformer( + private val onRefreshClick: () -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val tangemPayState = TangemPayState.RefreshNeeded( + notification = TangemPayRefreshNeeded( + tangemIcon = R.drawable.ic_tangem_24, + onRefreshClick = onRefreshClick, + ), + ) + return prevState.copy(tangemPayState = tangemPayState) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt new file mode 100644 index 0000000000..b7fc00db85 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState + +internal object TangemPayUnavailableStateTransformer : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + tangemPayState = TangemPayState.TemporaryUnavailable( + notification = WalletNotification.Warning.TangemPayUnreachable, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index d5c4aa78bf..d38fbba5b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.R import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -30,7 +31,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo iconSize = 36.dp, subtitle = state.description, iconResId = state.iconRes, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + buttonsState = SecondaryButtonConfig( text = state.buttonText, onClick = state.onButtonClick, shouldShowProgress = state.showProgress, @@ -40,13 +41,15 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo } is TangemPayState.Card -> TangemPayCardMainBlock(state, isBalanceHidden, modifier) is TangemPayState.Empty -> Unit + is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier) + is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier) } } @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ResetCardScreenPreview() { +private fun TangemPayMainScreenBlockPreview() { TangemThemePreview { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt new file mode 100644 index 0000000000..e00d8de72e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt @@ -0,0 +1,77 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded + +@Composable +internal fun TangemPayRefreshBlock(state: TangemPayState.RefreshNeeded, modifier: Modifier = Modifier) { + Column(modifier) { + Notification( + config = state.notification.config, + iconTint = when (state.notification) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention + else -> null + }, + ) + SpacerH12() + + BlockCard( + modifier = Modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + ) { + InputRowImageBase( + modifier = Modifier.padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_payment_account_sync_needed), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayRefreshBlockPreview() { + TangemThemePreview { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + TangemPayRefreshBlock( + state = TangemPayState.RefreshNeeded( + TangemPayRefreshNeeded( + tangemIcon = R.drawable.ic_tangem_24, + onRefreshClick = {}, + ), + ), + modifier = Modifier, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemUnavailableBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemUnavailableBlock.kt new file mode 100644 index 0000000000..0c52dbff5b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemUnavailableBlock.kt @@ -0,0 +1,75 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.utils.StringsSigns.DASH_SIGN + +@Composable +internal fun TangemPayUnavailableBlock(state: TangemPayState.TemporaryUnavailable, modifier: Modifier = Modifier) { + Column(modifier) { + Notification( + config = state.notification.config, + iconTint = when (state.notification) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention + else -> null + }, + ) + SpacerH12() + + BlockCard( + modifier = Modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + ) { + InputRowImageBase( + modifier = Modifier.padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = TextReference.Str(DASH_SIGN), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPayUnavailableBlockPreview() { + TangemThemePreview { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + TangemPayUnavailableBlock( + state = TangemPayState.TemporaryUnavailable( + WalletNotification.Warning.TangemPayUnreachable, + ), + modifier = Modifier, + ) + } + } +} \ No newline at end of file