From 4032360f13712d57b3400c5f4af7eb5b6d21c473 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Sep 2025 16:59:15 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 16 ---- .../tangem/tap/routing/utils/ChildFactory.kt | 15 +++- .../com/tangem/common/routing/AppRoute.kt | 19 ++++- .../pay/models/response/CustomerMeResponse.kt | 24 ++++++ .../datasource/local/visa/TangemPayStorage.kt | 4 - .../src/main/res/drawable/ic_promo_kyc_36.xml | 23 ++++++ .../drawable/ic_tangem_pay_promo_card_36.xml | 18 ++++ .../drawable/img_tangem_pay_card_main_36.xml | 50 +++++++++++ .../res/drawable/img_visa_label_26_16.xml | 17 ++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 12 +++ .../repository/DefaultOnboardingRepository.kt | 32 ++++++-- .../repository/TangemPayRequestPerformer.kt | 44 +++++++--- .../tangem/domain/pay/model/CustomerInfo.kt | 21 +++-- .../pay/repository/OnboardingRepository.kt | 5 ++ .../TangemPayMainScreenCustomerInfoUseCase.kt | 15 ++++ .../com/tangem/features/kyc/KycComponent.kt | 11 ++- .../features/kyc/DefaultKycComponent.kt | 51 ++++++++---- .../tangem/features/kyc/DefaultKycModel.kt | 2 +- .../tangem/features/kyc/KycLoadingScreen.kt | 41 ++++++++++ features/kyc/mock/build.gradle.kts | 2 + .../tangem/features/kyc/MockKycComponent.kt | 10 ++- .../TangemPayOnboardingComponent.kt | 10 ++- .../DefaultTangemPayOnboardingComponent.kt | 18 ++-- .../DefaultOnboardVisaDeepLinkHandler.kt | 3 +- .../model/TangemPayOnboardingModel.kt | 36 +++++--- .../tangempay/ui/TandemPayOnboardingScreen.kt | 9 +- features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/model/WalletModel.kt | 28 +++++++ .../wallet/state/model/TangemPayState.kt | 23 ++++++ .../wallet/state/model/WalletScreenState.kt | 1 + .../converter/TangemPayStateConverter.kt | 52 ++++++++++++ .../presentation/wallet/ui/WalletScreen.kt | 6 ++ .../singlecurrency/TangemPayCardMainBlock.kt | 82 +++++++++++++++++++ .../visa/TangemPayMainScreenBlock.kt | 79 ++++++++++++++++++ 34 files changed, 679 insertions(+), 101 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_promo_kyc_36.xml create mode 100644 core/ui/src/main/res/drawable/ic_tangem_pay_promo_card_36.xml create mode 100644 core/ui/src/main/res/drawable/img_tangem_pay_card_main_36.xml create mode 100644 core/ui/src/main/res/drawable/img_visa_label_26_16.xml create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt create mode 100644 features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/KycLoadingScreen.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.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 f990fdc0fd..19171195db 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -14,7 +14,6 @@ import javax.inject.Singleton import kotlin.text.encodeToByteArray private const val DEFAULT_KEY = "tangem_pay_default_key" -private const val DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY = "tangem_pay_default_customer_wallet_address_key" @Singleton internal class DefaultTangemPayStorage @Inject constructor( @@ -54,21 +53,6 @@ internal class DefaultTangemPayStorage @Inject constructor( ?.let(tokensAdapter::fromJson) } - /** - * Store the only customer wallet address, since for the f&f user can issue only one card tied to one address - */ - override suspend fun storeCustomerWalletAddress(customerWalletAddress: String) = - withContext(dispatcherProvider.io) { - secureStorage.store( - customerWalletAddress.encodeToByteArray(throwOnInvalidSequence = true), - DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY, - ) - } - - override suspend fun getCustomerWalletAddress(): String? = withContext(dispatcherProvider.io) { - secureStorage.get(DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY)?.decodeToString(throwOnInvalidSequence = true) - } - override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { secureStorage.delete(createKey(customerWalletAddress)) } 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 04197aacb2..ebdf08b11a 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 @@ -16,6 +16,7 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.* +import com.tangem.features.kyc.KycComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -35,6 +36,7 @@ import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.v2.api.SendWithSwapComponent import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -106,6 +108,7 @@ internal class ChildFactory @Inject constructor( private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, + private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -597,10 +600,20 @@ internal class ChildFactory @Inject constructor( is AppRoute.TangemPayOnboarding -> { createComponentChild( context = context, - params = TangemPayOnboardingComponent.Params(route.deeplink), + params = when (val mode = route.mode) { + is AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding -> ContinueOnboarding + is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(deeplink = mode.deeplink) + }, componentFactory = tangemPayOnboardingComponentFactory, ) } + is AppRoute.Kyc -> { + createComponentChild( + context = context, + params = KycComponent.Params, + componentFactory = kycComponentFactory, + ) + } is AppRoute.YieldSupplyPromo -> { createComponentChild( context = context, 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 716edd6750..0a8969253b 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 @@ -376,8 +376,23 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class TangemPayOnboarding( - val deeplink: String, - ) : AppRoute(path = "/tangem_pay_onboarding/$deeplink") + val mode: Mode, + ) : AppRoute(path = "/tangem_pay_onboarding/$mode") { + + @Serializable + sealed class Mode { + @Serializable + data class Deeplink( + val deeplink: String, + ) : Mode() + + @Serializable + object ContinueOnboarding : Mode() + } + } + + @Serializable + data object Kyc : AppRoute(path = "/kyc") @Serializable data class YieldSupplyPromo( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index 6c60787d46..24b58f4b95 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.math.BigDecimal @JsonClass(generateAdapter = true) data class CustomerMeResponse( @@ -16,6 +17,8 @@ data class CustomerMeResponse( @Json(name = "product_instance") val productInstance: ProductInstance?, @Json(name = "payment_account") val paymentAccount: PaymentAccount?, @Json(name = "kyc") val kyc: Kyc?, + @Json(name = "card") val card: Card?, + @Json(name = "balance") val balance: Balance?, ) @JsonClass(generateAdapter = true) @@ -45,4 +48,25 @@ data class CustomerMeResponse( @Json(name = "review_answer") val reviewAnswer: String, @Json(name = "created_at") val createdAt: String, ) + + @JsonClass(generateAdapter = true) + data class Card( + @Json(name = "token") val token: String, + @Json(name = "expiration_month") val expirationMonth: Int, + @Json(name = "expiration_year") val expirationYear: Int, + @Json(name = "emboss_name") val embossName: String, + @Json(name = "card_type") val cardType: String, + @Json(name = "card_status") val cardStatus: String, + @Json(name = "card_number_end") val cardNumberEnd: String, + ) + + @JsonClass(generateAdapter = true) + data class Balance( + @Json(name = "currency") val currency: String, + @Json(name = "available_balance") val availableBalance: BigDecimal, + @Json(name = "credit_limit") val creditLimit: BigDecimal, + @Json(name = "pending_charges") val pendingCharges: BigDecimal, + @Json(name = "posted_charges") val postedCharges: BigDecimal, + @Json(name = "balance_due") val balanceDue: BigDecimal, + ) } \ No newline at end of file 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 b988222b09..edf36a599f 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 @@ -8,9 +8,5 @@ interface TangemPayStorage { suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? - suspend fun storeCustomerWalletAddress(customerWalletAddress: String) - - suspend fun getCustomerWalletAddress(): String? - suspend fun clear(customerWalletAddress: String) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_promo_kyc_36.xml b/core/ui/src/main/res/drawable/ic_promo_kyc_36.xml new file mode 100644 index 0000000000..54962fbcae --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_promo_kyc_36.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_pay_promo_card_36.xml b/core/ui/src/main/res/drawable/ic_tangem_pay_promo_card_36.xml new file mode 100644 index 0000000000..4fc9a9cf1c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_pay_promo_card_36.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_card_main_36.xml b/core/ui/src/main/res/drawable/img_tangem_pay_card_main_36.xml new file mode 100644 index 0000000000..1b23c9f415 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_tangem_pay_card_main_36.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_visa_label_26_16.xml b/core/ui/src/main/res/drawable/img_visa_label_26_16.xml new file mode 100644 index 0000000000..c5859a2fda --- /dev/null +++ b/core/ui/src/main/res/drawable/img_visa_label_26_16.xml @@ -0,0 +1,17 @@ + + + + diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 7519633126..a29a800638 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -5,9 +5,11 @@ import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository import com.tangem.data.pay.repository.DefaultOnboardingRepository import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -27,4 +29,14 @@ internal interface TangemPayDataModule { @Binds @Singleton fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository + + companion object { + @Provides + @Singleton + fun provideTangemPayMainScreenCustomerInfoUseCase( + repository: OnboardingRepository, + ): TangemPayMainScreenCustomerInfoUseCase { + return TangemPayMainScreenCustomerInfoUseCase(repository = repository) + } + } } \ 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 cd8d7634f2..0307d16174 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 @@ -5,8 +5,10 @@ import arrow.core.raise.either import com.tangem.core.error.UniversalError import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest +import com.tangem.datasource.api.pay.models.response.CustomerMeResponse +import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo -import com.tangem.domain.pay.model.ProductInstance +import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.repository.OnboardingRepository import javax.inject.Inject @@ -26,11 +28,31 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun getCustomerInfo(): Either = either { return requestHelper.request { authHeader -> tangemPayApi.getCustomerMe(authHeader) - }.map { - CustomerInfo( - productInstance = it.result?.productInstance?.let { ProductInstance(id = it.id, status = it.status) }, - kycStatus = it.result?.kyc?.status, + }.map { getCustomerInfo(it.result) } + } + + override suspend fun getMainScreenCustomerInfo(): Either = either { + return requestHelper.requestWithPersistedToken { authHeader -> + tangemPayApi.getCustomerMe(authHeader) + }.map { getCustomerInfo(it.result) } + } + + private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo { + val card = response?.card + val balance = response?.balance + val cardInfo = if (card != null && balance != null) { + CardInfo( + lastFourDigits = card.cardNumberEnd, + balance = balance.availableBalance, + currencyCode = balance.currency, ) + } else { + null } + return CustomerInfo( + productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) }, + kycStatus = response?.kyc?.status, + cardInfo = cardInfo, + ) } } \ No newline at end of file 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 dabdf56d8b..ccd82930e5 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 @@ -10,18 +10,20 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -35,9 +37,9 @@ internal class TangemPayRequestPerformer @Inject constructor( @NetworkMoshi moshi: Moshi, private val dispatchers: CoroutineDispatcherProvider, private val tangemPayStorage: TangemPayStorage, - private val userWalletsRepository: UserWalletsListRepository, private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase, private val authDataSource: TangemPayAuthDataSource, + private val getWalletsUseCase: GetWalletsUseCase, ) { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) @@ -49,22 +51,39 @@ internal class TangemPayRequestPerformer @Inject constructor( suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): Either = either { withContext(dispatchers.io) { - performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind() + performRequest( + requestBlock = requestBlock, + getTokens = ::getAccessTokens, + refreshTokens = ::refreshAuthTokens, + ).bind() } } + suspend fun requestWithPersistedToken( + requestBlock: suspend (header: String) -> ApiResponse, + ): Either = either { + withContext(dispatchers.io) { + performRequest( + requestBlock = requestBlock, + getTokens = ::getAccessTokensIfSaved, + refreshTokens = ::refreshAuthTokens, + ).bind() + } + } + private suspend fun performRequest( requestBlock: suspend (header: String) -> ApiResponse, + getTokens: (suspend () -> Either), refreshTokens: (suspend () -> Either)? = null, ): Either = either { runCatching { - requestBlock("Bearer ${getAccessTokens().bind().accessToken}").getOrThrow() + requestBlock("Bearer ${getTokens().bind().accessToken}").getOrThrow() }.getOrElse { error -> when (error) { is ApiResponseError.HttpException -> { if (refreshTokens != null && error.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) { refreshOrJoin(refreshTokens).bind() - performRequest(requestBlock, refreshTokens = null).bind() + performRequest(requestBlock, refreshTokens = null, getTokens = getTokens).bind() } else { raise(mapHttpError(error)) } @@ -103,9 +122,7 @@ internal class TangemPayRequestPerformer @Inject constructor( } private suspend fun getCustomerWalletAddress(): Either = either { - customerWalletAddress - ?: tangemPayStorage.getCustomerWalletAddress() - ?: fetchAuthInputData().bind().address + customerWalletAddress ?: fetchAuthInputData().bind().address } private suspend fun getAccessTokens(): Either = either { @@ -113,6 +130,11 @@ internal class TangemPayRequestPerformer @Inject constructor( tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind() } + private suspend fun getAccessTokensIfSaved(): Either = either { + tangemPayStorage.getAuthTokens(getCustomerWalletAddress().bind()) + ?: raise(VisaApiError.UnknownWithoutCode) + } + private fun mapHttpError(throwable: ApiResponseError.HttpException): UniversalError { val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode return runCatching { @@ -125,14 +147,16 @@ internal class TangemPayRequestPerformer @Inject constructor( } private suspend fun fetchAuthInputData(): Either = either { - val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold + val userWallets = getWalletsUseCase() + .filter { it.isNotEmpty() } + .first() + val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold ?: raise(VisaApiError.UnknownWithoutCode) val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE)) .getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode) customerWalletAddress = address - tangemPayStorage.storeCustomerWalletAddress(address) AuthInputData(address, wallet.cardId) } 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 b018ac10da..bee0c16152 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,19 +1,28 @@ package com.tangem.domain.pay.model +import java.math.BigDecimal + private const val APPROVED_KYC_STATUS = "APPROVED" private const val ACTIVE_PI_STATUS = "active" data class CustomerInfo( val productInstance: ProductInstance?, val kycStatus: String?, + val cardInfo: CardInfo?, ) { + data class ProductInstance( + val id: String, + val status: String, + ) + + data class CardInfo( + val lastFourDigits: String, + val balance: BigDecimal, + val currencyCode: String, + ) + fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS fun isProductInstanceActive() = productInstance?.status == ACTIVE_PI_STATUS -} - -data class ProductInstance( - val id: String, - val status: String, -) \ No newline at end of file +} \ 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 4838f46c53..d6a888bc10 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -9,4 +9,9 @@ interface OnboardingRepository { suspend fun validateDeeplink(link: String): Either suspend fun getCustomerInfo(): Either + + /** + * Returns only if the user already authorised at least once + */ + suspend fun getMainScreenCustomerInfo(): Either } \ 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 new file mode 100644 index 0000000000..cefdd9f95a --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.pay.usecase + +import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.repository.OnboardingRepository + +/** + * Returns tangem pay customer info for the main screen banner + * Works only if the user already authorised at least once (won't emit anything otherwise) + */ +class TangemPayMainScreenCustomerInfoUseCase( + private val repository: OnboardingRepository, +) { + + suspend operator fun invoke(): CustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull() +} \ No newline at end of file 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 6a18888000..97e2d1f0cc 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 @@ -1,12 +1,11 @@ package com.tangem.features.kyc -import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent -interface KycComponent { +interface KycComponent : ComposableContentComponent { - fun launch() + data object Params - interface Factory { - fun create(appComponentContext: AppComponentContext): KycComponent - } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index 1be34aa554..76713a61b2 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -1,45 +1,62 @@ package com.tangem.features.kyc +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.sumsub.sns.core.SNSMobileSDK import com.sumsub.sns.core.data.listener.TokenExpirationHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.pay.KycStartInfo import com.tangem.features.kyc.theme.TangemSNSIconHandler import com.tangem.features.kyc.theme.TangemSNSTheme import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import java.util.Locale class DefaultKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, + @Assisted params: KycComponent.Params, ) : KycComponent, AppComponentContext by appComponentContext { - private val model: DefaultKycModel = getOrCreateModel() + private val model: DefaultKycModel = getOrCreateModel(params) - override fun launch() { + init { componentScope.launch { - model.uiState.collect { - it?.let { startInfo -> - val tokenExpirationHandler = object : TokenExpirationHandler { - override fun onTokenExpired() = "" - } - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) - .withTheme(TangemSNSTheme.theme(activity)) - .withIconHandler(TangemSNSIconHandler()) - .withLocale(Locale(startInfo.locale)) - .build() - snsSdk.launch() - } + model.uiState.drop(1).collectLatest { startInfo -> + startInfo?.let { launchSdk(startInfo) } + router.pop() } } - model.getKycToken() + } + + private fun launchSdk(startInfo: KycStartInfo) { + val tokenExpirationHandler = object : TokenExpirationHandler { + /** + * We don't refresh this token for f&f release. Assume it lives long enough to finish KYC + * [REDACTED_TODO_COMMENT] + */ + override fun onTokenExpired() = "" + } + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) + .withTheme(TangemSNSTheme.theme(activity)) + .withIconHandler(TangemSNSIconHandler()) + .withLocale(Locale(startInfo.locale)) + .build() + snsSdk.launch() + } + + @Composable + override fun Content(modifier: Modifier) { + KycLoadingScreen(router::pop, modifier) } @AssistedFactory interface Factory : KycComponent.Factory { - override fun create(appComponentContext: AppComponentContext): DefaultKycComponent + override fun create(context: AppComponentContext, params: KycComponent.Params): DefaultKycComponent } } \ 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 21b8e8e9bb..7df782304f 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 @@ -21,7 +21,7 @@ class DefaultKycModel @Inject constructor( private val _uiState: MutableStateFlow = MutableStateFlow(null) val uiState = _uiState.asStateFlow() - fun getKycToken() { + init { modelScope.launch { kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) } } diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/KycLoadingScreen.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/KycLoadingScreen.kt new file mode 100644 index 0000000000..c96c285fc3 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/KycLoadingScreen.kt @@ -0,0 +1,41 @@ +package com.tangem.features.kyc + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun KycLoadingScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + topBar = { + AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), + onBackClick = onBack, + iconRes = R.drawable.ic_back_24, + ) + }, + content = { paddingValues -> + Box( + modifier = modifier + .padding(paddingValues) + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier, + color = TangemTheme.colors.icon.primary1, + ) + } + }, + ) +} \ No newline at end of file diff --git a/features/kyc/mock/build.gradle.kts b/features/kyc/mock/build.gradle.kts index a64c1f8a89..2ba00e1182 100644 --- a/features/kyc/mock/build.gradle.kts +++ b/features/kyc/mock/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { implementation(projects.features.kyc.api) implementation(projects.core.decompose) + implementation(deps.compose.ui) + implementation(projects.core.ui) /** DI */ implementation(deps.hilt.android) diff --git a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt index 71bd1f9b5d..7119fdd452 100644 --- a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt +++ b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt @@ -1,23 +1,25 @@ package com.tangem.features.kyc +import androidx.compose.runtime.Composable import com.tangem.core.decompose.context.AppComponentContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import androidx.compose.ui.Modifier /** * Mocking it for release/external builds to exclude SumSub dependency + * This will never be called if the FT [isTangemPayEnabled] is off */ internal class MockKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, ) : KycComponent, AppComponentContext by appComponentContext { - override fun launch() { - /* no op */ - } + @Composable + override fun Content(modifier: Modifier) { /* no op */ } @AssistedFactory interface Factory : KycComponent.Factory { - override fun create(appComponentContext: AppComponentContext): MockKycComponent + override fun create(context: AppComponentContext, params: KycComponent.Params): MockKycComponent } } \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index d2257362fb..0ad307aa79 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -5,9 +5,13 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface TangemPayOnboardingComponent : ComposableContentComponent { - data class Params( - val deeplink: String, - ) + sealed class Params { + data class Deeplink( + val deeplink: String, + ) : Params() + + object ContinueOnboarding : Params() + } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 58a1454848..01effd1f8b 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -5,33 +5,29 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.child -import com.tangem.features.kyc.KycComponent import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.tangempay.model.TangemPayOnboardingModel +import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: TangemPayOnboardingComponent.Params, - kycComponentFactory: KycComponent.Factory, ) : TangemPayOnboardingComponent, AppComponentContext by appComponentContext { - private val kycComponent = kycComponentFactory.create(child("kycComponent")) - private val model: TangemPayOnboardingModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { val state by model.screenState.collectAsStateWithLifecycle() - TandemPayOnboardingScreen(modifier = modifier, state = state, onButtonClick = ::onButtonClick) - } - - private fun onButtonClick() { - kycComponent.launch() + TandemPayOnboardingScreen( + modifier = modifier, + state = state, + onBackClick = model::back, + onOpenKycClick = model::openKyc, + ) } @AssistedFactory diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt index 3b10b702a9..15313ee912 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -16,7 +16,8 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( init { if (tangemPayFeatureToggles.isTangemPayEnabled) { - appRouter.push(AppRoute.TangemPayOnboarding(uri.toString())) + val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(uri.toString()) + appRouter.push(AppRoute.TangemPayOnboarding(mode)) } else { appRouter.push(AppRoute.Home()) } 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 61b942c805..bb4f1621ce 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 @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.model import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -30,29 +31,44 @@ internal class TangemPayOnboardingModel @Inject constructor( init { modelScope.launch { - repository.validateDeeplink(params.deeplink) - .onRight { isValid -> if (isValid) checkCustomerInfo() } - .onLeft { exit() } + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { + checkCustomerInfo() + } + is TangemPayOnboardingComponent.Params.Deeplink -> { + repository.validateDeeplink(params.deeplink) + .onRight { isValid -> if (isValid) checkCustomerInfo() } + .onLeft { back() } + } + } } } + fun openKyc() { + router.replaceAll(AppRoute.Wallet, AppRoute.Kyc) + } + + fun back() { + router.pop() + } + private suspend fun checkCustomerInfo() { repository.getCustomerInfo() .onRight { customerInfo -> when { !customerInfo.isKycApproved() -> { - screenState.value = screenState.value.copy(fullScreenLoading = false) + when (params) { + is TangemPayOnboardingComponent.Params.Deeplink -> + screenState.value = screenState.value.copy(fullScreenLoading = false) + else -> openKyc() + } } !customerInfo.isProductInstanceActive() -> { // TODO [REDACTED_TASK_KEY]: create order and poll order status (API is not ready yet) } - else -> exit() + else -> back() } } - .onLeft { exit() } - } - - private fun exit() { - router.pop() + .onLeft { back() } } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt index b0b18abdd6..7240b154e5 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt @@ -15,7 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun TandemPayOnboardingScreen( state: TangemPayOnboardingScreenState, - onButtonClick: () -> Unit, + onOpenKycClick: () -> Unit, + onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -23,7 +24,7 @@ internal fun TandemPayOnboardingScreen( topBar = { AppBarWithBackButton( modifier = Modifier.statusBarsPadding(), - onBackClick = {}, + onBackClick = onBackClick, iconRes = R.drawable.ic_back_24, ) }, @@ -33,7 +34,7 @@ internal fun TandemPayOnboardingScreen( .padding(paddingValues) .fillMaxSize(), state = state, - onButtonClick = onButtonClick, + onButtonClick = onOpenKycClick, ) }, ) @@ -44,6 +45,6 @@ internal fun TandemPayOnboardingScreen( @Composable private fun PreviewDarkTheme() { TangemThemePreview { - TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}) + TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}, {}) } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 9a04d605c3..d06b12cd86 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -116,6 +116,7 @@ dependencies { implementation(projects.features.kyc.api) implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) + implementation(projects.features.tangempay.details.api) /** Common modules */ implementation(projects.common) 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 4921d64c1e..3ea6b5b660 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.usecase.* @@ -33,12 +34,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -84,6 +87,9 @@ internal class WalletModel @Inject constructor( private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, + private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayStateConverter: TangemPayStateConverter, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -112,6 +118,7 @@ internal class WalletModel @Inject constructor( subscribeOnSelectedWalletFlow() subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() + subscribeToTangemPayInfo() enableNotificationsIfNeeded() clickIntents.initialize(innerWalletRouter, modelScope) @@ -313,6 +320,27 @@ internal class WalletModel @Inject constructor( .saveIn(clearNFTCacheJobHolder) } + private fun subscribeToTangemPayInfo() { + /** + * Update state each time a user opens/returns to wallet screen + */ + screenLifecycleProvider.isBackgroundState.onEach { isBackground -> + if (!isBackground) { updateTangemPayInfo() } + }.launchIn(modelScope) + } + + private fun updateTangemPayInfo() { + if (tangemPayFeatureToggles.isTangemPayEnabled) { + modelScope.launch { + tangemPayMainScreenCustomerInfoUseCase()?.let { info -> + stateHolder.update { + it.copy(tangemPayState = tangemPayStateConverter.convert(info)) + } + } + } + } + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) 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 new file mode 100644 index 0000000000..cced51be15 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class TangemPayState { + + object Empty : TangemPayState() + + data class Progress( + val title: TextReference, + val buttonText: TextReference, + @DrawableRes val iconRes: Int, + val onButtonClick: () -> Unit, + ) : TangemPayState() + + data class Card( + val lastFourDigits: TextReference, + val balanceText: TextReference, + ) : TangemPayState() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index df260b5856..e142c4af9d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -14,4 +14,5 @@ internal data class WalletScreenState( val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, val onDismissMarketsOnboarding: () -> Unit, + val tangemPayState: TangemPayState = TangemPayState.Empty, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt new file mode 100644 index 0000000000..b2afc216b5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.R +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.pay.model.CustomerInfo.CardInfo +import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.utils.converter.Converter +import java.util.Currency +import javax.inject.Inject + +internal class TangemPayStateConverter @Inject constructor( + private val router: Router, +) : Converter { + + override fun convert(value: CustomerInfo): TangemPayState { + val route = AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding) + val cardInfo = value.cardInfo + return when { + !value.isKycApproved() -> TangemPayState.Progress( + title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = { router.push(route) }, + ) + !value.isProductInstanceActive() -> TangemPayState.Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.Res(R.string.common_continue), + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = { router.push(route) }, + ) + cardInfo != null -> { + TangemPayState.Card( + lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), + balanceText = TextReference.Str(getBalanceText(cardInfo)), + ) + } + else -> TangemPayState.Empty + } + } + + private fun getBalanceText(cardInfo: CardInfo): String { + val currency = Currency.getInstance(cardInfo.currencyCode) + return cardInfo.balance.format { + fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 28adcb811c..073d32efd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -79,6 +79,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet +import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -212,6 +213,11 @@ private fun WalletContent( notifications(configs = selectedWallet.warnings, modifier = itemModifier) + item( + key = "TangemPayMainScreenBlock", + contentType = state.tangemPayState::class.java, + ) { TangemPayMainScreenBlock(state.tangemPayState, itemModifier) } + (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> walletState.marketPriceBlockState?.let { marketPriceBlockState -> marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt new file mode 100644 index 0000000000..7cc3d681e6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState + +@Composable +internal fun TangemPayCardMainBlock(state: TangemPayState.Card, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.img_tangem_pay_card_main_36), + contentDescription = null, + modifier = Modifier.size(36.dp).clip(CircleShape), + ) + + Spacer(Modifier.width(12.dp)) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_kyc_card_ready_notification_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + + Spacer(Modifier.height(6.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.img_visa_label_26_16), + contentDescription = null, + modifier = Modifier.size(26.dp, 16.dp), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = state.lastFourDigits.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.TopEnd, + ) { + Text( + text = state.balanceText.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) + } + } + } +} \ 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 new file mode 100644 index 0000000000..2f03ffe4e0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +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.extensions.TextReference +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.ui.components.singlecurrency.TangemPayCardMainBlock + +@Composable +internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier = Modifier) { + AnimatedVisibility(state !is TangemPayState.Empty) { + when (state) { + is TangemPayState.Progress -> { + Notification( + modifier = modifier + .fillMaxWidth(), + config = NotificationConfig( + title = state.title, + iconSize = 36.dp, + subtitle = TextReference.EMPTY, + iconResId = state.iconRes, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = state.buttonText, + onClick = state.onButtonClick, + ), + ), + ) + } + is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier) + is TangemPayState.Empty -> Unit + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ResetCardScreenPreview() { + TangemThemePreview { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + TangemPayMainScreenBlock( + TangemPayState.Progress( + title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = {}, + ), + ) + + TangemPayMainScreenBlock( + TangemPayState.Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.Res(R.string.common_continue), + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = {}, + ), + ) + + TangemPayMainScreenBlock( + TangemPayState.Card( + lastFourDigits = TextReference.Str("*1234"), + balanceText = TextReference.Str("$ 0.00"), + ), + ) + } + } +} \ No newline at end of file