From f192c85914f1ad31e36271cfacbe8ea1ee71dbf6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 12:05:48 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../account/AccountIconItemStateConverter.kt | 31 +- .../CreatePaymentAccountNotification.kt | 98 +++++ .../com/tangem/datasource/di/MoshiModule.kt | 15 +- ...usDM.kt => PaymentAccountStatusValueDM.kt} | 45 ++- .../drawable/img_tangem_pay_visa_banner.webp | Bin 0 -> 3402 bytes data/account/build.gradle.kts | 4 + .../DefaultSingleAccountListProducer.kt | 39 +- .../DefaultSingleAccountListProducerTest.kt | 8 + .../PaymentAccountStatusDMConverter.kt | 67 ---- .../PaymentAccountStatusValueDMConverter.kt | 139 +++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 6 +- .../DefaultPaymentAccountStatusFetcher.kt | 179 +++++---- .../DefaultPaymentAccountStatusProducer.kt | 16 +- .../repository/DefaultOnboardingRepository.kt | 18 +- .../pay/store/PaymentAccountStatusesStore.kt | 45 ++- .../domain/account/models/AccountList.kt | 16 +- domain/account/status/build.gradle.kts | 3 + .../DefaultSingleAccountStatusListProducer.kt | 125 ++++-- .../tangem/domain/models/account/Account.kt | 11 +- .../domain/models/account/AccountStatus.kt | 3 +- .../account/PaymentAccountStatusValue.kt | 193 ++++++++++ .../tangem/domain/pay/PaymentAccountStatus.kt | 66 ---- .../pay/flow/PaymentAccountStatusProducer.kt | 4 +- .../pay/flow/PaymentAccountStatusSupplier.kt | 12 +- .../tangem/domain/pay/model/CustomerInfo.kt | 5 + .../TangemPayMainScreenCustomerInfoUseCase.kt | 24 +- .../selector/PortfolioSelectorModel.kt | 2 +- .../tokenlist/model/OnrampTokenListModel.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 3 +- features/tangempay/main/api/build.gradle.kts | 2 + .../component/TangemPayMainBlockComponent.kt | 19 + .../tangempay/entity/TangemPayMainUM.kt | 26 ++ features/tangempay/main/impl/build.gradle.kts | 3 +- .../DefaultTangemPayMainBlockComponent.kt | 37 ++ .../tangempay/di/TangemPayMainModule.kt | 17 + .../tangempay/ui/TangemPayMainBlockContent.kt | 355 ++++++++++++++++++ features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/WalletComponent.kt | 14 +- .../wallet/child/wallet/model/WalletModel.kt | 29 +- .../model/intents/TangemPayClickIntents.kt | 26 +- .../preview/WalletScreenPreviewDataLegacy.kt | 3 + .../utils/WalletWarningsAnalyticsSender.kt | 1 + .../domain/GetMultiWalletWarningsFactory.kt | 118 +++--- .../wallet/state/model/WalletNotification.kt | 10 +- .../wallet/state/model/WalletState.kt | 7 + .../transformers/AddWalletTransformer.kt | 2 + .../InitializeWalletsTransformer.kt | 2 + .../ReinitializeNewWalletTransformer.kt | 2 + .../ReinitializeWalletTransformer.kt | 2 + .../transformers/SetTokenListTransformer.kt | 19 + .../TangemPayRefreshNeededStateTransformer.kt | 1 - ...TangemPayRefreshShowProgressTransformer.kt | 12 +- .../transformers/UnlockWalletTransformer.kt | 2 + .../converter/TangemPayMainBlockConverter.kt | 110 ++++++ .../state/utils/WalletLoadingStateFactory.kt | 6 + .../presentation/wallet/ui/WalletScreen.kt | 52 ++- .../components/common/WalletNotifications.kt | 13 + .../visa/TangemPayMainScreenBlock.kt | 38 +- .../components/visa/TangemPayRefreshBlock.kt | 1 - 59 files changed, 1679 insertions(+), 432 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt rename core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/{PaymentAccountStatusDM.kt => PaymentAccountStatusValueDM.kt} (52%) create mode 100644 core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt delete mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt create mode 100644 features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt create mode 100644 features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 6cc4b2bf7e..8c3453b855 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -8,23 +8,20 @@ import com.tangem.utils.converter.Converter class AccountIconItemStateConverter( val size: AccountIconSize = AccountIconSize.Default, -) : Converter { +) : Converter { - override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { - is Account.CryptoPortfolio -> when { - value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter( - char = value.accountName.toUM().value, - color = value.icon.color.getUiColor(), - isGrayscale = false, - size = size, - ) - else -> CurrencyIconState.CryptoPortfolio.Icon( - resId = value.icon.value.getResId(), - color = value.icon.color.getUiColor(), - isGrayscale = false, - size = size, - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + override fun convert(value: Account.CryptoPortfolio): CurrencyIconState.CryptoPortfolio = when { + value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter( + char = value.accountName.toUM().value, + color = value.icon.color.getUiColor(), + isGrayscale = false, + size = size, + ) + else -> CurrencyIconState.CryptoPortfolio.Icon( + resId = value.icon.value.getResId(), + color = value.icon.color.getUiColor(), + isGrayscale = false, + size = size, + ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt new file mode 100644 index 0000000000..a681d0df23 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt @@ -0,0 +1,98 @@ +package com.tangem.common.ui.notifications + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.notifications.CloseableIconButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.ForceDarkTheme +import com.tangem.core.ui.res.TangemTheme + +private const val GRADIENT_START_COLOR = 0xFF252934 +private const val GRADIENT_END_COLOR = 0xFF12141E +private const val GRADIENT_OFFSET_X = 164f +private const val GRADIENT_OFFSET_Y = 39f +private const val GRADIENT_RADIUS = 82f + +@Composable +fun CreatePaymentAccountNotification( + onClick: () -> Unit, + onCloseClick: () -> Unit, + @DrawableRes image: Int, + title: TextReference, + subtitle: TextReference, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background( + brush = Brush.radialGradient( + colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)), + center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y), + radius = GRADIENT_RADIUS, + ), + ) + .clickable(onClick = onClick), + ) { + Image( + modifier = Modifier.size(78.dp), + painter = painterResource(id = image), + contentDescription = null, + ) + Column( + modifier = Modifier + .padding(start = 78.dp, top = 12.dp, end = 12.dp, bottom = 12.dp) + .align(Alignment.CenterStart), + ) { + Text( + modifier = Modifier.padding(end = TangemTheme.dimens.size32), + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.constantWhite, + ) + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + CloseableIconButton( + onClick = onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.inactive, + ) + } +} + +@Preview(widthDp = 360) +@Composable +private fun CreatePaymentAccountNotification_Preview() { + ForceDarkTheme { + CreatePaymentAccountNotification( + onClick = {}, + onCloseClick = {}, + image = R.drawable.img_tangem_pay_visa_banner, + title = resourceReference(R.string.tangempay_onboarding_banner_title), + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index de63c7bbcd..effeecade9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,7 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -47,13 +47,12 @@ class MoshiModule { .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) .add( - NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) - .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") - .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") - .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") - .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") - .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") - .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java) + .withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card") + .withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"), ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index 589fb4d915..7b1ca88f44 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -11,43 +11,58 @@ import java.math.BigDecimal /** * Payment account status for storage in the local cache. * - * @see [com.tangem.domain.pay.PaymentAccountStatus] + * @see [com.tangem.domain.models.account.AccountStatus.Payment] */ @JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) -sealed interface PaymentAccountStatusDM { +sealed interface PaymentAccountStatusValueDM { @NameLabel("not_created") data class NotCreated( @Json(name = "not_created") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM @NameLabel("kyc_status") data class UnderReview( @Json(name = "kyc_status") val kycStatus: KycStatus, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM @NameLabel("issuing_card") data class IssuingCard( @Json(name = "issuing_card") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM - @NameLabel("locked") - data class Locked( - @Json(name = "locked") val marker: Boolean = true, - ) : PaymentAccountStatusDM - - @NameLabel("balance") - data class Loaded( + @NameLabel("active_card") + data class ActiveCard( + @Json(name = "active_card") val isLocked: Boolean, + @Json(name = "customer_id") val customerId: String, @Json(name = "card_id") val cardId: String, @Json(name = "last_four_digits") val lastFourDigits: String, - @Json(name = "balance") val balance: BigDecimal, @Json(name = "currency_code") val currencyCode: String, @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "is_pin_set") val isPinSet: Boolean, - ) : PaymentAccountStatusDM + @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, + @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + ) : PaymentAccountStatusValueDM @NameLabel("card_issue_failed") data class CardIssueFailed( @Json(name = "card_issue_failed") val marker: Boolean = true, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM + + @JsonClass(generateAdapter = true) + data class FiatBalanceDM( + @Json(name = "available_balance") val availableBalance: BigDecimal, + @Json(name = "currency") val currency: String, + ) + + @JsonClass(generateAdapter = true) + data class CryptoBalanceDM( + @Json(name = "id") val id: String, + @Json(name = "chain_id") val chainId: Long, + @Json(name = "deposit_address") val depositAddress: String, + @Json(name = "token_contract_address") val tokenContractAddress: String, + @Json(name = "balance") val balance: BigDecimal, + ) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp new file mode 100644 index 0000000000000000000000000000000000000000..5768fd6820284ab1721fdeb6cdce23e26767e9ad GIT binary patch literal 3402 zcmV-Q4Yl%8Nk&FO4FCXFMM6+kP&il$0000G0000t0RT4v06|PpNY(%V00D4S+ip}L zp%Nki36X$=IsgfEfP_duLM0#}5-K5oZ|q8$Z$v0^+qPl%xFzi)y3JPLKll&+ga1x0 zG{nfvJ<+DZbFr7DsOFp(k(5%5!*+>cpY2CQb$t^>CJIeA+Q;RmQOqK9N>hw2jiOkc zrKrA$VxvCn*{UzC`mjfh_bB=7dmPY7*ak7duJ6?m{|m57sMYJc8aK!(-lcXMZztzCWD_LjUQTcw$b)3ma6 zt^Qn|Y1sk98Hcv4{QF@6K0Pr?ZG1b2?50KR3DTI zOf^=vX6-Y%cH+S_?NC@>&@SFNuD>vVsSMILOep1e+4}fq+fZ}*e|h}CR3O?O2=!4t zHkL+Y#h9n41^U!uS(%s)vQS(+ar_Jqm}v0eRMCP*6h4^XVRrQ5KN$JQ@y+V6j#r)B z+4`(%O+E-mHct?~6CYb`5m((7h-bT#an~}7SED1>BKN;mRh8kZ=RU6^)MXR5@{)_m z^gI{YPcv4cPf6hshSOWo)Z(*M(qMji7YoND_~qeBT(S3z1Sv z`CGfYxwIj9+MR)jp7|EaYsMw0tyDi>lR$jlJLRgQvKvkXr2l+kgH99d+p5ve!obj) ze!fqvta4#Pq(9p&lQ7F{p0!m~QWbiCj_9TTAU8!;`jyOeEoe01MV z!J+Q-l4}Ia@`|+%D4R`hlCE`+(MS>7+jOm zJM=?4zaq}+O|tc5VCr;(Qe`Uj2qb-dix>}P_?C$=j_I z_UA$B5FlwO|MVsj~XqS$tI^f$AeUOkHh6s#W|~o2(4C=@}@i`!}Vk*Iw>^$ zP^)jPHWI}|<+3Yr3hrG^+0C;5Fm<{<|ibX(4 zpn#Mhg_$^i5ui!QszzRGxc@6>E4_bWl4oJ8Q12}f0^9^2hRdUwG`%NwqdS$IM;yj2 z*?^=W{od$=GigC2O?S$T4aya4z#G%CHR;gpO5dhHS-YyLm5!cZNS}S&o zH|M8PnY-6&?CLH#yZ2~(f-n64;9}dl7*!^1;wPZs>ixrpjbO|m5W%CById#%^gW+u z2OwO96OC)09sH7iwRItZ84 zBb}Az8hREp7P-$J>#@|2f5b)5O^S++WYf#aw``dCsCQ`fN_4;Zhuk&~!v5nQtWAYV z8gosBa6n7dTM}w!IW2|42L0BUif#sxtdE4EPHwc*ejrK9Fr&Mn#WA(-Sh*vh%z7f! z-yjy)4lGKQ|EF%jYY3k~-PkBz6$KPY0S-e*1;dUjM zrK*d=GgrQPk5gIU>Gg?+I~KkY`ANaCPV}=Qr?j1l)UJ9xd+E(P-0Z_iA*6{m(02sE zLG<3->c;-%l@S)u(^rgq{*n|YWcpK?rNd;)OLM1n8pK!o5eNq5La=5rSK8|PuTsSE zyPcozzITmTUCO;G<&(fMA6xl2NnV$CU8@;WKT z_rzyL1H+og-B^v+-Nc~1RWe(MXI8;a=DfggOk*9DYFHw8Q$ zH5L1@yLL|tkRV^VQ+!#$Ys9}}nI@(Z>IlWP=Etprl?2=-YF+tH9(+JN03StytOI9j z@-bSHBw}Dv8<$|a@u?dG=3pBfpe3@iex3ws^|#G_2-QKFDU+cW+CK@hry#=n*umG@ z9za_UzVUos2RzbG7iSZ}GeQ}?5}T@@19Nzk^G%1K1CORBxZ6jI2j|m?v*2vhZ%6#^ z_wAa4O==!k9$kbo_i}1hs?{~&L@w`IUOt&))S~A5QeYNe(M|Pmj$gjaWJXvC{sCeA zUZRv9j!{YWzxO+p8xA~EXDbesKAgn+RHff(WZJb~q!cs6vibgyWcY#mx+n~>NEfSW z2le?lop~qfPj@xbrw#IhFDh{vzM0M4wAOHQ2Ye>91mYXAQGxS#Z0F-xG+I*ASSrYYUD6(G6$DDT5Gz+THvQ-a4~~2bDo#RZlZxR9E!n{d z@)l4?m11-Gz}7S=Az*gV{hVzJ3p`g1Sgbhka1htg+lBOQs=B;J`69k(tuCc zFP{{AFU#3}2E+5%y?ex~U0L=~S@XmzAw57Mw{`}r|r{s@#+l4C1)ypzZk zT*GnaLLF`sI_ShU#fg`hDUlgPwPo|&Ue_v_H7h0aYcA{#hso?1tyB8-vVkcav(>62v8d(CJ_ zehbu-9(zPN(nyssD*f|jCf>lh|DEbB)3kT*y>>WvQ-!o`s)X9W7To&Kf7|Ys<{KHT zWHrB@BS#%!r#WDWdw3D*&ESLC`b`Q!G+8k1MLpEzN5YJz1|GOPM3$``Mr35zqiPLA?dCkwo=V!**c%d@r>@6Ti(Bw*#%W7mz1 zubGgOOU@6Che;LF=Uv56_}H~qlA?SfI8f(^8N5P!(&6fJ-y=ma)Mx;e{Avn4c8h^X g^6Voz=J0g>T&C5~f}kMVu~@{rd-&df5c~iD0IiawMgRZ+ literal 0 HcmV?d00001 diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 61664f301e..9f3fa0d8a4 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -33,8 +33,11 @@ dependencies { api(projects.domain.models) api(projects.domain.tokens) api(projects.domain.wallets) + api(projects.domain.visa) // endregion + implementation(projects.features.tangempay.details.api) // Remove after TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + // region Project - Data implementation(projects.data.common) // endregion @@ -47,6 +50,7 @@ dependencies { // region Tangem dependencies implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) // endregion // region DI diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 8eb7e90e0d..28fd5fd6be 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -1,10 +1,18 @@ package com.tangem.data.account.producer import arrow.core.Option +import arrow.core.getOrElse import arrow.core.none +import com.tangem.common.card.FirmwareVersion import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.tangempay.TangemPayFeatureToggles +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -12,6 +20,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map /** * Default implementation of [SingleAccountListProducer]. @@ -27,6 +36,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @Assisted val params: SingleAccountListProducer.Params, override val flowProducerTools: FlowProducerTools, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountListProducer { @@ -34,8 +45,32 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { - return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) - .flowOn(dispatchers.default) + val accountListFlow: Flow = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) { + combineWithPaymentAccount() + } else { + walletAccountListFlowFactory.create(userWalletId = params.userWalletId) + } + + return accountListFlow.flowOn(dispatchers.default) + } + + private fun combineWithPaymentAccount(): Flow { + return walletAccountListFlowFactory.create(params.userWalletId) + .map { accountList -> + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) + if (userWallet.isPaymentAccountSupported()) { + accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable -> + error("Can not combine account list and payment account status: $throwable") + } + } else { + accountList + } + } + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } @AssistedFactory diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt index b1069afb7c..4837e4f2a6 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt @@ -3,10 +3,12 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -29,6 +31,10 @@ class DefaultSingleAccountListProducerTest { private val userWalletId = UserWalletId("011") private val flowProducerTools: FlowProducerTools = mockk() + private val tangemPayFeatureToggles = mockk { + every { this@mockk.isTangemPayAccountsRefactorEnabled } returns false + } + private val userWalletsListRepository = mockk() private val userWallet = mockk { every { this@mockk.walletId } returns userWalletId } @@ -38,6 +44,8 @@ class DefaultSingleAccountListProducerTest { walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), flowProducerTools = flowProducerTools, + tangemPayFeatureToggles = tangemPayFeatureToggles, + userWalletsListRepository = userWalletsListRepository, ) @AfterEach diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt deleted file mode 100644 index db6fbcca4f..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.data.pay.converter - -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus -import com.tangem.utils.converter.TwoWayConverter - -/** - * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. - * - * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted - * (Loading, ExposedDevice, Unavailable, NotSynced). - * - * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. - */ -internal object PaymentAccountStatusDMConverter : - TwoWayConverter { - - override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { - return when (value) { - is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() - is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) - is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() - is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() - is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() - // Transient statuses are not persisted - is PaymentAccountStatus.Loading, - is PaymentAccountStatus.Error.ExposedDevice, - is PaymentAccountStatus.Error.Unavailable, - is PaymentAccountStatus.Error.NotSynced, - -> null - } - } - - override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { - return when (value) { - is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed - is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated - is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) - is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) - is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( - source = StatusSource.CACHE, - kycStatus = value.kycStatus, - ) - is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( - source = StatusSource.CACHE, - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) - } - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt new file mode 100644 index 0000000000..a42852436a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusValueDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { + return when (value) { + is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated() + is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview( + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard() + is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = true, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = false, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed( + customerId = value.customerId, + ) + // Transient statuses are not persisted + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.Error.Unavailable, + is PaymentAccountStatusValue.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { + return when (value) { + is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated + is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed( + customerId = value.customerId, + ) + is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard( + source = StatusSource.CACHE, + ) + is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) { + PaymentAccountStatusValue.Locked( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } else { + PaymentAccountStatusValue.Loaded( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } + is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + null -> PaymentAccountStatusValue.Error.Unavailable + } + } + + private fun PaymentAccountStatusValue.FiatBalance.toDM(): PaymentAccountStatusValueDM.FiatBalanceDM { + return PaymentAccountStatusValueDM.FiatBalanceDM( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValue.CryptoBalance.toDM(): PaymentAccountStatusValueDM.CryptoBalanceDM { + return PaymentAccountStatusValueDM.CryptoBalanceDM( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } + + private fun PaymentAccountStatusValueDM.FiatBalanceDM.toDomain(): PaymentAccountStatusValue.FiatBalance { + return PaymentAccountStatusValue.FiatBalance( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValueDM.CryptoBalanceDM.toDomain(): PaymentAccountStatusValue.CryptoBalance { + return PaymentAccountStatusValue.CryptoBalance( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } +} \ No newline at end of file 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 c1842bbec1..3887f44e6f 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 @@ -15,10 +15,9 @@ import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher @@ -32,6 +31,7 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module @@ -118,7 +118,7 @@ internal interface TangemPayDataModule { persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( moshi = moshi, - types = mapWithStringKeyTypes(), + types = mapWithStringKeyTypes(), defaultValue = emptyMap(), ), produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 7342c3da2a..57cc801313 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -2,17 +2,19 @@ package com.tangem.data.pay.flow import arrow.core.Either import com.tangem.data.pay.store.PaymentAccountStatusesStore -import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -29,87 +31,101 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusFetcher { + private val logger = TangemLogger.withTag(TAG) + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = - eitherOn(dispatchers.default) { - TangemLogger.withTag(TAG).i("fetch: ${params.userWalletId.stringValue}") + Either.catchOn(dispatchers.default) { + val account = Account.Payment(userWalletId = params.userWalletId) + logger.i("fetch: ${params.userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - TangemLogger.withTag( - TAG, - ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") - return@eitherOn paymentAccountStatusesStore.store( + return@catchOn paymentAccountStatusesStore.store( userWalletId = params.userWalletId, - status = PaymentAccountStatus.Error.ExposedDevice, + status = AccountStatus.Payment( + account = account, + value = PaymentAccountStatusValue.Error.ExposedDevice, + ), ) } val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) .fold( ifLeft = { error -> - TangemLogger.withTag( - TAG, - ).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") when (error) { - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } }, ifRight = { hasTangemPay -> - proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + proceedHasTangemPayResult(account = account, hasTangemPay = hasTangemPay) }, ) - TangemLogger.withTag(TAG).i("invoke status ${params.userWalletId}: $status") - paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + logger.i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = AccountStatus.Payment(account = account, value = status), + ) + }.onLeft { + paymentAccountStatusesStore.updateStatusSource( + userWalletId = params.userWalletId, + source = StatusSource.ONLY_CACHE, + ) } private suspend fun proceedHasTangemPayResult( - userWalletId: UserWalletId, + account: Account.Payment, hasTangemPay: Boolean, - ): PaymentAccountStatus { - TangemLogger.withTag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + ): PaymentAccountStatusValue { + logger.i("proceedHasTangemPayResult for ${account.userWalletId} hasTangemPay: $hasTangemPay") return if (hasTangemPay) { - fetchTangemPayAccountStatus(userWalletId = userWalletId) + fetchTangemPayAccountStatus(account) } else { - PaymentAccountStatus.NotCreated + PaymentAccountStatusValue.NotCreated } } - private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { - val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) - if (prevResult == null || prevResult is PaymentAccountStatus.Error) { - paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId) + if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error) { + paymentAccountStatusesStore.store( + userWalletId = account.userWalletId, + status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading), + ) } - return proceedWithOrderId(userWalletId = userWalletId) + return proceedWithOrderId(account = account) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { - return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { - PaymentAccountStatus.Error.NotSynced + private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue { + return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) { + PaymentAccountStatusValue.Error.NotSynced } else { - val orderId = onboardingRepository.getOrderId(userWalletId) + val orderId = onboardingRepository.getOrderId(account.userWalletId) if (orderId != null) { - proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + proceedWithOrderId(account = account, orderId = orderId) } else { - proceedWithoutOrder(userWalletId = userWalletId) + proceedWithoutOrder(account = account) } } } - private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { - return onboardingRepository.getCustomerInfo(userWalletId).fold( + private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue { + return onboardingRepository.getCustomerInfo(account.userWalletId).fold( ifLeft = { error -> - TangemLogger.withTag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") error.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> - TangemLogger.withTag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") val status = customerInfo.mapToPaymentAccountStatus() - if (customerInfo.productInstance == null) { - onboardingRepository.createOrder(userWalletId) + if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(account.userWalletId) .onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") } } status @@ -117,63 +133,94 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { - return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue { + return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold( ifLeft = { error -> - TangemLogger.withTag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { orderData -> - TangemLogger.withTag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}") when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, - -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) OrderStatus.CANCELED -> { - PaymentAccountStatus.Error.CardIssueFailed + PaymentAccountStatusValue.Error.CardIssueFailed(customerId = orderData.customerId) } OrderStatus.COMPLETED -> { // Order was completed -> clear order id and get customer info - onboardingRepository.clearOrderId(userWalletId) - onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + onboardingRepository.clearOrderId(account.userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( ifLeft = { it.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, ) } - OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable } }, ) } - private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { - PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) - } else if (cardInfo != null && productInstance != null) { - PaymentAccountStatus.Loaded( + PaymentAccountStatusValue.UnderReview( source = StatusSource.ACTUAL, - cardId = productInstance.cardId, - lastFourDigits = cardInfo.lastFourDigits, - balance = cardInfo.balance, - currencyCode = cardInfo.currencyCode, - depositAddress = cardInfo.depositAddress, - isPinSet = cardInfo.isPinSet, + kycStatus = kycStatus, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, + ) + } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) { + convertToContentState( + productInstance = productInstance, + cardInfo = cardInfo, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, ) } else { - PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) } } - private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun convertToContentState( + productInstance: CustomerInfo.ProductInstance, + cardInfo: CustomerInfo.CardInfo, + customerId: String, + ): PaymentAccountStatusValue { + return when (productInstance.frozenState) { + TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + else -> PaymentAccountStatusValue.Loaded( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue { return when (this) { - is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt index cb021c1ee8..c2c53ae7cd 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -4,8 +4,9 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.flow.FlowProducerTools -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -21,12 +22,15 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusProducer { - override val fallback: Option - get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() - override fun produce(): Flow { + private val account = Account.Payment(userWalletId = params.userWalletId) + + override val fallback: Option + get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some() + + override fun produce(): Flow { return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) - .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) } .flowOn(dispatchers.default) } 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 8e20d83b04..6880483883 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 @@ -13,6 +13,7 @@ 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.TangemPayEligibilityType +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -139,6 +140,7 @@ internal class DefaultOnboardingRepository @Inject constructor( ?: error("no userWallet found") } + @Suppress("ComplexCondition") private suspend fun getCustomerInfo( userWalletId: UserWalletId, response: CustomerMeResponse.Result?, @@ -148,14 +150,26 @@ internal class DefaultOnboardingRepository @Inject constructor( val card = response?.card val fiatBalance = response?.balance?.fiat + val cryptoBalance = response?.balance?.crypto val paymentAccount = response?.paymentAccount - val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { + val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) { CardInfo( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, + fiatBalance = PaymentAccountStatusValue.FiatBalance( + availableBalance = fiatBalance.availableBalance, + currency = fiatBalance.currency, + ), + cryptoBalance = PaymentAccountStatusValue.CryptoBalance( + id = cryptoBalance.id, + chainId = cryptoBalance.chainId.toLong(), + depositAddress = cryptoBalance.depositAddress.orEmpty(), + tokenContractAddress = cryptoBalance.tokenContractAddress, + balance = cryptoBalance.balance, + ), ) } else { null @@ -167,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId) + ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState) } return CustomerInfo( customerId = response?.id, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index d42e7623a1..ec8a48d880 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -1,11 +1,14 @@ package com.tangem.data.pay.store import androidx.datastore.core.DataStore -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope @@ -14,8 +17,8 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch -internal typealias WalletIdWithPaymentStatus = Map -internal typealias WalletIdWithPaymentStatusDM = Map +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map /** * Store for payment account statuses with dual storage (runtime + persistence). @@ -26,7 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map, private val persistenceDataStore: DataStore, - private val scope: AppCoroutineScope, + scope: AppCoroutineScope, ) { init { @@ -34,8 +37,10 @@ internal class PaymentAccountStatusesStore( try { val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch runtimeStore.store( - value = cachedStatuses.mapValues { (_, statusDM) -> - PaymentAccountStatusDMConverter.convertBack(statusDM) + value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> + val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) + val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM) + AccountStatus.Payment(account = account, value = statusValue) }, ) } catch (e: Exception) { @@ -44,18 +49,28 @@ internal class PaymentAccountStatusesStore( } } - fun get(userWalletId: UserWalletId): Flow { + fun get(userWalletId: UserWalletId): Flow { return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } } - suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? { return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) } - suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) { + runtimeStore.update(emptyMap()) { stored -> + stored.toMutableMap().apply { + val paymentAccountStatus = this[userWalletId.stringValue] ?: return@update stored + val newValue = paymentAccountStatus.copy(value = paymentAccountStatus.value.copySealed(source = source)) + put(key = userWalletId.stringValue, value = newValue) + } + } + } + + suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) { coroutineScope { launch { storeInRuntime(userWalletId = userWalletId, status = status) } - launch { storeInPersistence(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status.value) } } } @@ -63,7 +78,7 @@ internal class PaymentAccountStatusesStore( return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) } - private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { put(key = userWalletId.stringValue, value = status) @@ -71,8 +86,8 @@ internal class PaymentAccountStatusesStore( } } - private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { - val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) { + val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return persistenceDataStore.updateData { storedStatuses -> storedStatuses.toMutableMap().apply { put(key = userWalletId.stringValue, value = statusDM) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index c413c7e46f..ab66eca0fd 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -108,16 +108,14 @@ data class AccountList private constructor( } fun flattenMapCurrencies(): Map = buildMap { - accounts.forEach { acc -> - val account = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") + accounts + .filterIsInstance() + .forEach { account -> + account.cryptoCurrencies.forEach { currency -> + val key = account.accountId to currency.id + put(key, currency) + } } - account.cryptoCurrencies.forEach { currency -> - val key = account.accountId to currency.id - put(key, currency) - } - } } /** diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 7c6c759fa8..20d40f33d4 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(projects.domain.staking) api(projects.domain.tokens) api(projects.domain.tokens.models) + api(projects.domain.visa) api(projects.domain.walletManager) api(projects.domain.wallets) @@ -39,6 +40,8 @@ dependencies { implementation(deps.kotlin.serialization) implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) // region DI implementation(deps.hilt.android) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 5250d458de..1c341528d0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -3,6 +3,7 @@ package com.tangem.domain.account.status.producer import arrow.core.Option import arrow.core.none import arrow.core.toOption +import com.tangem.common.card.FirmwareVersion import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.domain.account.models.AccountCurrencyId import com.tangem.domain.account.models.AccountList @@ -33,6 +34,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceProducer @@ -42,6 +44,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.domain.tokens.operations.PriceChangeCalculator import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -69,6 +72,7 @@ import java.math.BigDecimal * [REDACTED_AUTHOR] */ +// TODO: Move to :data:account:status [REDACTED_JIRA] @Suppress("LongParameterList") @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @@ -76,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo override val flowProducerTools: FlowProducerTools, private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, private val networkStatusSupplier: MultiNetworkStatusSupplier, @@ -116,31 +121,52 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency = flattenCurrency, ) - combine( + if (userWallet.isPaymentAccountSupported()) { + combineWithPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId), + ) + } else { + combineWithoutPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + ) + } + .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + paymentAccountStatusFlow: Flow, + ): Flow { + return combine( flow = accountListFlow, flow2 = cryptoCurrencyStatusFlow, - transform = { accountList, currencyStatusMap -> - val accountStatuses: List = accountList.accounts.map { acc -> - val account: Account.CryptoPortfolio = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - if (account.cryptoCurrencies.isEmpty()) { - account.toEmptyAccountStatus() - } else { - val statuses: List = account.cryptoCurrencies.map { currency -> - val acId = account.accountId to currency.id - currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + flow3 = paymentAccountStatusFlow, + transform = { accountList, currencyStatusMap, paymentAccountStatus -> + val accountStatuses = accountList.accounts.map { account -> + when (account) { + is Account.Payment -> paymentAccountStatus + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) } - AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenListFactory.create( - statuses = statuses, - groupType = accountList.groupType, - sortType = accountList.sortType, - ), - priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), - ) } } val balances = accountStatuses.flattenTotalFiatBalance() @@ -156,7 +182,58 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ) }, ) - .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithoutPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + ): Flow { + return combine( + flow = accountListFlow, + flow2 = cryptoCurrencyStatusFlow, + transform = { accountList, currencyStatusMap -> + val accountStatuses = accountList.accounts + .filterIsInstance() + .map { account -> + when (account) { + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) + } + } + } + val balances = accountStatuses.flattenTotalFiatBalance() + + AccountStatusList( + userWalletId = accountList.userWalletId, + accountStatuses = accountStatuses, + totalAccounts = accountList.totalAccounts, + totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), + totalArchivedAccounts = accountList.totalArchivedAccounts, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) + }, + ) + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } private fun ProducerScope.flattenCurrencyStatusFlow( @@ -278,7 +355,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo return map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance + is AccountStatus.Payment -> accountStatus.value.totalFiatBalance } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index a0442452ef..79ab7d3f5a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -178,12 +178,15 @@ sealed interface Account { @Serializable data class Payment( override val accountId: AccountId, - override val accountName: AccountName, - val cryptoCurrencies: List, ) : Account { + override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse { + error("Can not create account name for Payment account with userWalletId = ${accountId.userWalletId}") + } - init { - error("Not yet implemented") + companion object { + operator fun invoke(userWalletId: UserWalletId): Payment { + return Payment(accountId = AccountId.forPaymentAccount(userWalletId = userWalletId)) + } } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index 31df2e7281..748f8de199 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -1,7 +1,6 @@ package com.tangem.domain.models.account import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList @@ -43,7 +42,7 @@ sealed interface AccountStatus { @Serializable data class Payment( override val account: Account.Payment, - val totalFiatBalance: TotalFiatBalance, + val value: PaymentAccountStatusValue, ) : AccountStatus } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt new file mode 100644 index 0000000000..fc44ccc105 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -0,0 +1,193 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +/** + * Represents the various states a payment account can have, encapsulating different information based on the state. + * + * @property source The source of the status information. + */ +@Serializable +sealed class PaymentAccountStatusValue { + abstract val source: StatusSource + + /** The total fiat balance associated with this status. */ + val totalFiatBalance: TotalFiatBalance + get() = when (this) { + is Error, + is IssuingCard, + is NotCreated, + is UnderReview, + -> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source) + is Loading -> TotalFiatBalance.Loading + is Locked -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + } + + /** + * Copies the status with a new [source]. + * + * @param source The new source of the status information. + */ + fun copySealed(source: StatusSource): PaymentAccountStatusValue { + return when (this) { + is IssuingCard -> copy(source = source) + is Loaded -> copy(source = source) + is Locked -> copy(source = source) + is UnderReview -> copy(source = source) + is Loading, + is NotCreated, + is Error, + -> this + } + } + + /** Represents the Loading state of a payment account, typically while fetching its details. */ + @Serializable + data object Loading : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Represents a state where the payment account has not been created yet. */ + @Serializable + data object NotCreated : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Represents a state where the payment account is under review (KYC). + * + * @property source The source of the status information. + * @property kycStatus The current KYC status. + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + val customerId: String, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the card for the payment account is being issued. + * + * @property source The source of the status information. + */ + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is locked. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Locked( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is successfully loaded with complete information. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Loaded( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** Represents an error state for the payment account status. */ + @Serializable + sealed class Error : PaymentAccountStatusValue() { + /** Error state indicating the device is exposed. */ + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account is unavailable. */ + @Serializable + data object Unavailable : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account data is not synced. */ + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Error state indicating that card issuance failed. + * + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class CardIssueFailed(val customerId: String) : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } + + /** + * Represents the fiat balance of the payment account. + * + * @property availableBalance The amount of available balance in fiat. + * @property currency The currency of the balance. + */ + @Serializable + data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String) + + /** + * Represents the crypto balance of the payment account. + * + * @property id The unique identifier of the crypto asset. + * @property chainId The identifier of the blockchain network. + * @property depositAddress The address for deposits. + * @property tokenContractAddress The contract address of the token. + * @property balance The amount of the crypto balance. + */ + @Serializable + data class CryptoBalance( + val id: String, + val chainId: Long, + val depositAddress: String, + val tokenContractAddress: String, + val balance: SerializedBigDecimal, + ) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt deleted file mode 100644 index bdc604087f..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.pay - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.serialization.SerializedBigDecimal -import kotlinx.serialization.Serializable - -@Serializable -sealed class PaymentAccountStatus { - - abstract val source: StatusSource - - @Serializable - data object Loading : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object NotCreated : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class UnderReview( - override val source: StatusSource, - val kycStatus: KycStatus, - ) : PaymentAccountStatus() - - @Serializable - data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Locked(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Loaded( - override val source: StatusSource, - val cardId: String, - val lastFourDigits: String, - val balance: SerializedBigDecimal, - val currencyCode: String, - val depositAddress: String?, - val isPinSet: Boolean, - ) : PaymentAccountStatus() - - @Serializable - sealed class Error : PaymentAccountStatus() { - @Serializable - data object ExposedDevice : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class Unavailable(override val source: StatusSource) : Error() - - @Serializable - data object NotSynced : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object CardIssueFailed : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - } -} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt index c49a25f45d..313e5741ce 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -1,10 +1,10 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus -interface PaymentAccountStatusProducer : FlowProducer { +interface PaymentAccountStatusProducer : FlowProducer { data class Params(val userWalletId: UserWalletId) interface Factory : FlowProducer.Factory diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt index 94580d26e1..11744a357c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -1,10 +1,18 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow @Suppress("UnnecessaryAbstractClass") abstract class PaymentAccountStatusSupplier( override val factory: PaymentAccountStatusProducer.Factory, override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + operator fun invoke(userWalletId: UserWalletId): Flow { + val params = PaymentAccountStatusProducer.Params(userWalletId) + return this.invoke(params) + } +} \ 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 2fd02e7a45..11ef7e59c7 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,6 +1,8 @@ package com.tangem.domain.pay.model +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.visa.model.TangemPayCardFrozenState import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -25,6 +27,7 @@ data class CustomerInfo( data class ProductInstance( val id: String, val cardId: String, + val frozenState: TangemPayCardFrozenState, ) data class CardInfo( @@ -33,5 +36,7 @@ data class CustomerInfo( val currencyCode: String, val depositAddress: String?, val isPinSet: Boolean, + val fiatBalance: PaymentAccountStatusValue.FiatBalance, + val cryptoBalance: PaymentAccountStatusValue.CryptoBalance, ) } \ 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 511457acb6..180b1b864f 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 @@ -15,8 +15,6 @@ import com.tangem.security.isSecurityExposed import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* -private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" - class TangemPayMainScreenCustomerInfoUseCase( private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, @@ -27,15 +25,15 @@ class TangemPayMainScreenCustomerInfoUseCase( val state: StateFlow>> field = MutableStateFlow(value = mapOf()) + private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase") + suspend fun fetch(userWalletId: UserWalletId) { - TangemLogger.withTag(TAG).i("fetch: ${userWalletId.stringValue}") + logger.i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - TangemLogger.withTag( - TAG, - ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) return // fast exit @@ -44,9 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase( onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> - TangemLogger.withTag( - TAG, - ).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") + logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") if (error is VisaApiError.NotPaeraCustomer) { showOnboardingBannerIfEligible(userWalletId) } else { @@ -54,7 +50,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } }, ifRight = { hasTangemPay -> - TangemLogger.withTag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") + logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay") if (hasTangemPay) { val oldResult = state.value[userWalletId] if (oldResult == null) { @@ -129,11 +125,11 @@ class TangemPayMainScreenCustomerInfoUseCase( ): Either { return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> - TangemLogger.withTag(TAG).e("mapErrorForCustomer: $error") + logger.e("mapErrorForCustomer: $error") error.mapErrorForCustomer() } .map { customerInfo -> - TangemLogger.withTag(TAG).i("customerInfo") + logger.i("customerInfo") if (customerInfo.productInstance == null) { onboardingRepository.createOrder(userWalletId) MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index d7e351ff96..b344d503d8 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -184,7 +184,7 @@ internal class PortfolioSelectorModel @Inject constructor( val account = accountStatus.account val accountBalance = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance + is AccountStatus.Payment -> accountStatus.value.totalFiatBalance } val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index ddc4c9751a..5b62caa0f6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -105,7 +106,7 @@ internal class OnrampTokenListModel @Inject constructor( updateTokenListUM( SetLoadingAccountTokenListTransformer( appCurrency = appCurrency, - accountList = accountList.accountStatuses.toList(), + accountList = accountList.accountStatuses.filterCryptoPortfolio().toList(), isAccountsMode = isAccountsMode, ), ) @@ -237,6 +238,7 @@ internal class OnrampTokenListModel @Inject constructor( private fun AccountStatusList.filterAccountsByQuery( query: String, ): Map> = accountStatuses.asSequence() + .filterCryptoPortfolio() .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 96f8ec12bf..14b10c2aad 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -31,6 +31,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -131,7 +132,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(userWalletId), - )?.accountStatuses.orEmpty() + )?.accountStatuses.orEmpty().filterCryptoPortfolio() val walletAccountCurrencyStatusesExceptInitial: Map> = walletAccountCurrencyStatuses.mapNotNull { accountStatus -> diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts index 15fb515b8b..3ae2de80dd 100644 --- a/features/tangempay/main/api/build.gradle.kts +++ b/features/tangempay/main/api/build.gradle.kts @@ -15,4 +15,6 @@ dependencies { /** Compose */ implementation(deps.compose.runtime) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) } \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..a38b7fca5d --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.features.tangempay.entity.TangemPayMainUM + +@Stable +interface TangemPayMainBlockComponent { + + fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt new file mode 100644 index 0000000000..977ce0927e --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.tangempay.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +sealed class TangemPayMainUM { + + data object Empty : TangemPayMainUM() + data object Loading : TangemPayMainUM() + data class UnderReview(val subtitle: TextReference, val onClick: () -> Unit) : TangemPayMainUM() + data class IssuingCard(val onClick: () -> Unit) : TangemPayMainUM() + data class FailedToIssue(val onClick: () -> Unit) : TangemPayMainUM() + data class Content( + val subtitle: TextReference, + val isBalanceFlickering: Boolean, + val balance: TextReference, + val balanceSubtitle: TextReference, + val onClick: () -> Unit, + val shouldShowOnlyCacheWarning: Boolean, + ) : TangemPayMainUM() + + data object TemporaryUnavailable : TangemPayMainUM() + data object SyncNeeded : TangemPayMainUM() + data object ExposedDevice : TangemPayMainUM() +} \ No newline at end of file diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts index eb442c8f69..c1aeff44a2 100644 --- a/features/tangempay/main/impl/build.gradle.kts +++ b/features/tangempay/main/impl/build.gradle.kts @@ -15,10 +15,9 @@ dependencies { /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.configToggles) /** Features api */ - implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.main.api) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..716d22e5f7 --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.ui.TangemPayMainBlockItem +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +private const val TANGEM_PAY_ACCOUNT_CONTENT_TYPE = "TangemPayAccount" + +@Suppress("UnusedPrivateProperty") +internal class DefaultTangemPayMainBlockComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: Unit, +) : TangemPayMainBlockComponent, AppComponentContext by context { + + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + item( + key = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + contentType = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + ) { + TangemPayMainBlockItem(state, isBalanceHidden, modifier) + } + } + + @AssistedFactory + interface Factory : TangemPayMainBlockComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultTangemPayMainBlockComponent + } +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt new file mode 100644 index 0000000000..c7dc8b22ab --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt @@ -0,0 +1,17 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.component.DefaultTangemPayMainBlockComponent +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayMainModule { + @Binds + fun bindTangemPayMainBlockComponent( + factory: DefaultTangemPayMainBlockComponent.Factory, + ): TangemPayMainBlockComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt new file mode 100644 index 0000000000..e95dcc262b --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt @@ -0,0 +1,355 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.main.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TangemPayMainUM.Empty -> Unit + is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier) + is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier) + is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier) + is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier) + is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier) + is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier) + is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier) + is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier) + } +} + +@Composable +private fun TangemPayMainBlockContent( + state: TangemPayMainUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + onClick = state.onClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.End, + ) { + TangemPayFiatAmount( + text = state.balance.resolveReference(), + isBalanceFlickering = state.isBalanceFlickering, + isBalanceFromCache = state.shouldShowOnlyCacheWarning, + isBalanceHidden = isBalanceHidden, + ) + Text( + text = state.balanceSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +private fun TangemPayFiatAmount( + text: String, + isBalanceFlickering: Boolean, + isBalanceFromCache: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(isBalanceFromCache) { + Row( + modifier = Modifier.padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } + } + + Text( + text = text.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.body2.applyBladeBrush( + isEnabled = isBalanceFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + textAlign = TextAlign.End, + ) + } +} + +@Composable +private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = state.subtitle, + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + 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, + ) + } +} + +@Composable +private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_issuing_your_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = TextReference.Res(R.string.tangempay_payment_account), + caption = TextReference.Res(R.string.tangempay_failed_to_issue_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36, + iconEndRes = R.drawable.ic_alert_24, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + 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, + ) + } +} + +@Composable +private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(modifier = Modifier.size(36.dp)) + Column( + modifier = Modifier.padding(start = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 70.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 52.dp, minHeight = 12.dp), + ) + } + SpacerWMax() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +private fun TangemPayMainItemsPreview( + @PreviewParameter(TangemPayMainUMPreviewParameterProvider::class) + state: TangemPayMainUM, +) { + TangemThemePreview { + TangemPayMainBlockItem(state = state, isBalanceHidden = false) + } +} + +private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemPayMainUM.Loading, + TangemPayMainUM.SyncNeeded, + TangemPayMainUM.TemporaryUnavailable, + TangemPayMainUM.ExposedDevice, + TangemPayMainUM.FailedToIssue(onClick = {}), + TangemPayMainUM.UnderReview(subtitle = resourceReference(R.string.tangempay_kyc_in_progress), onClick = {}), + TangemPayMainUM.IssuingCard(onClick = {}), + TangemPayMainUM.Content( + subtitle = TextReference.Str("*1234"), + isBalanceFlickering = true, + balance = TextReference.Str("$ 101.56"), + balanceSubtitle = TextReference.Str("USDC"), + onClick = {}, + shouldShowOnlyCacheWarning = true, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 7fcdc3dc2e..e3f92c0e20 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -148,6 +148,7 @@ dependencies { implementation(projects.features.tangempay.details.api) implementation(projects.features.feed.api) implementation(projects.features.promoBanners.api) + implementation(projects.features.tangempay.main.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index a62274f509..22f857a5f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -37,6 +37,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.send.v2.api.NetworkSelectionComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent @@ -51,6 +52,7 @@ internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, feedEntryComponentFactory: FeedEntryComponent.Factory, + tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, @@ -70,6 +72,12 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } + private val tangemPayMainBlockComponent by lazy { + tangemPayMainBlockComponentFactory.create( + context = child("tangemPayMainBlockComponent"), + params = Unit, + ) + } private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy { if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null @@ -218,10 +226,11 @@ internal class WalletComponent @AssistedInject constructor( val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() + val uiState by model.uiState.collectAsStateWithLifecycle() if (designFeatureToggles.isRedesignEnabled) { WalletScreen2( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, @@ -234,8 +243,9 @@ internal class WalletComponent @AssistedInject constructor( ) } else { WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, promoBannersBlockComponent = promoBannersBlockComponent, + tangemPayComponent = tangemPayMainBlockComponent, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, 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 1d3096be23..16d2963864 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 @@ -6,13 +6,13 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -25,20 +25,20 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.* import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.qrscanning.models.QrResultSource -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.qrscanning.models.ClassifiedQrContent -import com.tangem.domain.qrscanning.models.QrSendTarget -import com.tangem.domain.walletconnect.WcPairService -import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase +import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import com.tangem.domain.qrscanning.models.QrResultSource +import com.tangem.domain.qrscanning.models.QrSendTarget +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.walletconnect.WcPairService +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.wallets.usecase.* -import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -61,6 +61,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider @@ -119,6 +120,8 @@ internal class WalletModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val wcPairService: WcPairService, private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -427,14 +430,17 @@ internal class WalletModel @Inject constructor( updateTangemPayJobHolder.cancel() modelScope.launch { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) while (isActive) { delay(TANGEM_PAY_UPDATE_INTERVAL) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.saveIn(updateTangemPayJobHolder) } else { // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.launchIn(modelScope) } @@ -543,6 +549,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, isMainScreenQrScanningEnabled = walletFeatureToggles.isMainScreenQrScanningEnabled, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -589,6 +596,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -610,6 +618,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -624,6 +633,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -685,6 +695,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) 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 1a2d2f0885..efa8fe4574 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 @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.TangemPayEntryPoint +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -30,7 +31,6 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer import kotlinx.coroutines.launch import javax.inject.Inject @@ -77,6 +77,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, ) : BaseWalletClickIntents(), TangemPayIntents { override suspend fun onPullToRefresh() { @@ -85,20 +86,28 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } override fun onRefreshPayToken(userWallet: UserWallet) { - stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId)) + stateHolder.update( + TangemPayRefreshShowProgressTransformer( + userWalletId = userWallet.walletId, + shouldShowProgress = true, + ), + ) modelScope.launch { produceInitialDataTangemPay.invoke(userWallet.walletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWallet.walletId)) + } .onLeft { stateHolder.update( - transformer = TangemPayRefreshNeededStateTransformer( - userWallet = userWallet, + TangemPayRefreshShowProgressTransformer( userWalletId = userWallet.walletId, - onRefreshClick = { onRefreshPayToken(userWallet) }, + shouldShowProgress = false, ), ) } @@ -267,7 +276,10 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled()) modelScope.launch { tangemPayOnboardingRepository.disableTangemPay(userWalletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } .onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 5485587450..91a230aa1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -217,6 +218,8 @@ internal object WalletScreenPreviewDataLegacy { onClick = {}, ), type = WalletType.Cold, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = false, ) } 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 63e329b73f..09f8eba96e 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 @@ -123,6 +123,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.UpgradeHotWalletPromo -> null is WalletNotification.TokenSyncCompleted -> null + is WalletNotification.CreateTangemPayAccount -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 2438cafa5a..e81d00263d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -7,10 +7,10 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -18,6 +18,8 @@ import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -25,7 +27,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map - import javax.inject.Inject @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @@ -69,13 +69,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - - val accountStatusListFlow by lazy { - val params = SingleAccountStatusListProducer.Params(userWallet.walletId) - accountDependencies.singleAccountStatusListSupplier(params) - .map { it.totalFiatBalance to it.flattenCurrencies() } - .map { Lce.Content(it) } - } + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( accountStatusListFlow, @@ -95,9 +90,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), ) { array -> array } .map { array -> - val lceTokens = array[0] as Lce>> - val totalFiatBalance = lceTokens.map { it.first } - val flattenCurrencies = lceTokens.map { it.second } + val accountStatusList = array[0] as AccountStatusList val isReadyToShowRating = array[1] as Boolean val isNeedToBackup = array[2] as Boolean val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus @@ -108,8 +101,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowUpgradeBanner = array[8] as Boolean val closureTimestamp = array[9] as? Long + val flattenCurrencies = accountStatusList.flattenCurrencies() + val paymentAccountStatus = accountStatusList.accountStatuses + .filterIsInstance() + .firstOrNull() + buildList { - addUsedOutdatedDataNotification(totalFiatBalance) + addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) @@ -162,24 +160,54 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( if (!hasCriticalOrWarning) { addRateTheAppNotification(isReadyToShowRating, clickIntents) } + + // add as last warning + paymentAccountStatus?.let { paymentAccountStatus -> + addTangemPayWarnings( + status = paymentAccountStatus, + userWallet = userWallet, + walletClickIntents = clickIntents, + ) + } }.toImmutableList() } } - private fun MutableList.addUsedOutdatedDataNotification( - totalFiatBalance: Lce, + private fun MutableList.addTangemPayWarnings( + status: AccountStatus.Payment, + userWallet: UserWallet, + walletClickIntents: WalletClickIntents, ) { + val notification = when (status.value) { + is PaymentAccountStatusValue.Error.NotSynced -> WalletNotification.Warning.TangemPayRefreshNeeded( + buttonText = when (userWallet) { + is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) + is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) + }, + onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, + shouldShowProgress = false, + ) + is PaymentAccountStatusValue.NotCreated -> WalletNotification.CreateTangemPayAccount( + onClick = { walletClickIntents.onOnboardingBannerClick(userWallet.walletId) }, + onCloseClick = { walletClickIntents.onOnboardingBannerCloseClick(userWallet.walletId) }, + ) + is PaymentAccountStatusValue.Error.Unavailable -> WalletNotification.Warning.TangemPayUnreachable + is PaymentAccountStatusValue.Error.CardIssueFailed, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.IssuingCard, + is PaymentAccountStatusValue.Loaded, + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Locked, + is PaymentAccountStatusValue.UnderReview, + -> null + } + notification?.let(::add) + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { addIf( element = WalletNotification.UsedOutdatedData, - condition = totalFiatBalance.fold( - ifLoading = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifContent = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifError = { false }, - ), + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, ) } @@ -254,7 +282,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addInformationalNotifications( userWallet: UserWallet, cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { addIf( @@ -267,7 +295,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addMissingAddressesNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val currencies = flattenCurrencies.getMissingAddressCurrencies() @@ -285,10 +313,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.getMissingAddressCurrencies(): List { - val flattenCurrencies = getOrNull(isPartialContentAccepted = true) ?: return emptyList() - - return flattenCurrencies + private fun List.getMissingAddressCurrencies(): List { + return this .filter { it.value is CryptoCurrencyStatus.MissedDerivation } .map(CryptoCurrencyStatus::currency) } @@ -330,7 +356,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, isNeedToBackup: Boolean, clickIntents: WalletClickIntents, ) { @@ -355,7 +381,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } private fun MutableList.addCloreMigrationNotification( - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return @@ -367,10 +393,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { - val currencies = getOrNull(isPartialContentAccepted = true) ?: return null - - return currencies.find { currencyStatus -> + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return this.find { currencyStatus -> BlockchainUtils.isClore(currencyStatus.currency.network.rawId) } } @@ -388,10 +412,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.hasUnreachableNetworks(): Boolean { - val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false - - return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable } + private fun List.hasUnreachableNetworks(): Boolean { + return this.any { it.value is CryptoCurrencyStatus.Unreachable } } // Remove in first iteration of yield supply feature @@ -427,7 +449,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldAccessCodeSkipped: Boolean, ) { @@ -437,12 +459,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired - - val type = flattenCurrencies.fold( - ifLoading = { return }, - ifContent = { it.getFinishWalletActivationType() }, - ifError = { WalletActivationBannerType.Attention }, - ) + val type = flattenCurrencies.getFinishWalletActivationType() addIf( element = WalletNotification.FinishWalletActivation( @@ -465,15 +482,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() - val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } + val hasBalance = flattenCurrencies.any { it.value.amount.orZero().isPositive() } val shouldShow = checkHotWalletUpgradeBannerUseCase( walletId = userWallet.walletId, 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 e3110857f1..77f9417e93 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 @@ -151,7 +151,6 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class TangemPayRefreshNeeded( - @DrawableRes private val tangemIcon: Int?, private val onRefreshClick: () -> Unit, private val buttonText: TextReference, private val shouldShowProgress: Boolean, @@ -160,7 +159,7 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), buttonsState = ButtonsState.PrimaryButtonConfig( text = buttonText, - iconResId = tangemIcon, + iconResId = R.drawable.ic_tangem_24, onClick = onRefreshClick, shouldShowProgress = shouldShowProgress, ), @@ -480,4 +479,11 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data class CreateTangemPayAccount(val onClick: () -> Unit, val onCloseClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + iconResId = R.drawable.img_tangem_pay_visa_banner, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 12261f3059..9459402f44 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTx import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -24,6 +25,8 @@ internal sealed interface WalletState : WalletStateHolder { abstract val nftState: WalletNFTItemUM abstract val type: WalletType abstract val tangemPayState: TangemPayState + abstract val tangemPayMainUM: TangemPayMainUM + abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -35,6 +38,8 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM, override val type: WalletType, override val tangemPayState: TangemPayState, + override val tangemPayMainUM: TangemPayMainUM, + override val isTangemPayRefactorEnabled: Boolean, ) : MultiCurrency() data class Locked( @@ -54,6 +59,8 @@ internal sealed interface WalletState : WalletStateHolder { override val tokensListState = WalletTokensListState.ContentState.Locked override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty + override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty + override val isTangemPayRefactorEnabled: Boolean = false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index ec3d6943be..7cba602ffb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -13,6 +13,7 @@ internal class AddWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -20,6 +21,7 @@ internal class AddWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index a92b59ba44..4c669daeed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -28,6 +28,7 @@ internal class InitializeWalletsTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isMainScreenQrScanningEnabled: Boolean = false, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -35,6 +36,7 @@ internal class InitializeWalletsTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index 0c1b198d0c..ed1b3a1b10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -24,6 +24,7 @@ internal class ReinitializeNewWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -31,6 +32,7 @@ internal class ReinitializeNewWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 6dab085776..00a8977b27 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -20,6 +20,7 @@ internal class ReinitializeWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { @@ -27,6 +28,7 @@ internal class ReinitializeWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4d5d813ddc..4c3d0689dc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability @@ -8,10 +9,12 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayMainBlockConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import com.tangem.utils.logging.TangemLogger +import com.tangem.features.tangempay.entity.TangemPayMainUM import java.math.BigDecimal internal class SetTokenListTransformer( @@ -25,12 +28,17 @@ internal class SetTokenListTransformer( private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { + private val tangemPayConverter by lazy { + TangemPayMainBlockConverter(tangemPayClickIntents = clickIntents) + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> { prevState.copy( walletCardState = prevState.walletCardState.toLoadedState(), tokensListState = prevState.tokensListState.toLoadedState(), + tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(), buttons = prevState.enableButtons(), ) } @@ -97,6 +105,17 @@ internal class SetTokenListTransformer( ).convert(value = this) } + private fun TangemPayMainUM.toLoadedState(): TangemPayMainUM { + val paymentAccountStatus = when (params) { + is TokenConverterParams.Account -> params.accountList.accountStatuses + .filterIsInstance() + .firstOrNull() + is TokenConverterParams.Wallet -> null + } ?: return this + + return tangemPayConverter.convert(paymentAccountStatus) + } + private fun toLoadedState(): WalletTokensListUM { if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( 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 index e2c4d1a6c4..b8673b8774 100644 --- 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 @@ -18,7 +18,6 @@ internal class TangemPayRefreshNeededStateTransformer( override fun transform(prevState: WalletState): WalletState { val tangemPayState = TangemPayState.RefreshNeeded( notification = TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = when (userWallet) { is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index b37a6f916f..17ff78b37f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -5,9 +5,11 @@ 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.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import kotlinx.collections.immutable.toImmutableList internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, + private val shouldShowProgress: Boolean, ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -15,11 +17,19 @@ internal class TangemPayRefreshShowProgressTransformer( val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState val refreshNotification = refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState + val newWarnings = prevState.warnings.map { warning -> + if (warning is WalletNotification.Warning.TangemPayRefreshNeeded) { + warning.copy(shouldShowProgress = shouldShowProgress) + } else { + warning + } + } return multiContentState.copy( tangemPayState = refreshNeededState.copy( - notification = refreshNotification.copy(shouldShowProgress = true), + notification = refreshNotification.copy(shouldShowProgress = shouldShowProgress), ), + warnings = newWarnings.toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 2c1c7e542f..6e44868ca7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -18,6 +18,7 @@ internal class UnlockWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -25,6 +26,7 @@ internal class UnlockWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt new file mode 100644 index 0000000000..41b74bc551 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import java.util.Currency + +private const val POLYGON_CHAIN_ID = 137 + +internal class TangemPayMainBlockConverter( + private val tangemPayClickIntents: TangemPayIntents, +) : Converter { + @Suppress("LongMethod", "CyclomaticComplexMethod") + override fun convert(value: AccountStatus.Payment): TangemPayMainUM { + return when (val statusValue = value.value) { + is PaymentAccountStatusValue.Error.CardIssueFailed -> TangemPayMainUM.FailedToIssue( + onClick = { tangemPayClickIntents.onIssuingFailedClicked(statusValue.customerId) }, + ) + is PaymentAccountStatusValue.Error.ExposedDevice -> TangemPayMainUM.ExposedDevice + is PaymentAccountStatusValue.Error.NotSynced -> TangemPayMainUM.SyncNeeded + is PaymentAccountStatusValue.Error.Unavailable -> TangemPayMainUM.TemporaryUnavailable + is PaymentAccountStatusValue.IssuingCard -> TangemPayMainUM.IssuingCard( + onClick = { tangemPayClickIntents.onIssuingCardClicked() }, + ) + is PaymentAccountStatusValue.UnderReview -> TangemPayMainUM.UnderReview( + subtitle = when (statusValue.kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + onClick = { + when (statusValue.kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = value.account.userWalletId, + customerId = statusValue.customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(value.account.userWalletId) + } + }, + ) + is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty + is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading + is PaymentAccountStatusValue.Locked -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Frozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + is PaymentAccountStatusValue.Loaded -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + } + } + + private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference { + val currency = Currency.getInstance(currencyCode) + val formattedBalance = balance.format { + fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + } + return stringReference(formattedBalance) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 06d9f69510..cb033c72d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -17,6 +17,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -33,6 +34,7 @@ internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) { fun create(userWallet: UserWallet): WalletState { @@ -82,6 +84,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Hot, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } @@ -96,6 +100,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Cold, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } 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 7e0a0747aa..8f842b82cf 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 @@ -49,7 +49,6 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.expressTransactionsItems -import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy @@ -60,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.softLayerShadow import com.tangem.core.ui.extensions.stringResourceSafe @@ -84,6 +84,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -92,6 +94,7 @@ import kotlin.math.roundToInt @Composable internal fun WalletScreen( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, promoBannersBlockComponent: ComposableContentComponent? = null, bottomSheetContent: @Composable (() -> Unit), bottomSheetHeaderHeightProvider: () -> Dp, @@ -106,6 +109,7 @@ internal fun WalletScreen( WalletContent( state = state, + tangemPayComponent = tangemPayComponent, walletsListState = walletsListState, snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, @@ -128,6 +132,7 @@ internal fun WalletScreen( @Composable private fun WalletContent( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, @@ -220,18 +225,12 @@ private fun WalletContent( } } - if (selectedWallet is WalletState.MultiCurrency) { - item( - key = "TangemPayMainScreenBlock", - contentType = selectedWallet.tangemPayState::class.java, - ) { - TangemPayMainScreenBlock( - state = selectedWallet.tangemPayState, - isBalanceHidden = state.isHidingMode, - modifier = itemModifier, - ) - } - } + tangemPayItem( + modifier = itemModifier, + state = selectedWallet, + isHidingMode = state.isHidingMode, + tangemPayComponent = tangemPayComponent, + ) (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> walletState.marketPriceBlockState?.let { marketPriceBlockState -> @@ -749,6 +748,25 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.tangemPayItem( + state: WalletState, + isHidingMode: Boolean, + tangemPayComponent: TangemPayMainBlockComponent, + modifier: Modifier = Modifier, +) { + if (state !is WalletState.MultiCurrency) return + + if (state.isTangemPayRefactorEnabled) { + with(tangemPayComponent) { + tangemPayMainContent(modifier = modifier, state = state.tangemPayMainUM, isBalanceHidden = isHidingMode) + } + } else { + item(key = "TangemPayMainScreenBlock", contentType = state.tangemPayState::class.java) { + TangemPayMainScreenBlock(modifier = modifier, state = state.tangemPayState, isBalanceHidden = isHidingMode) + } + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { @@ -767,6 +785,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, + tangemPayComponent = object : TangemPayMainBlockComponent { + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + } + }, bottomSheetContent = { Text("Markets Content") }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 90fc34c41f..8d058b28bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.CreatePaymentAccountNotification import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -49,6 +52,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + CreatePaymentAccountNotification( + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + onClick = item.onClick, + onCloseClick = item.onCloseClick, + image = R.drawable.img_tangem_pay_visa_banner, + title = resourceReference(R.string.tangempay_onboarding_banner_title), + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + ) + } else -> { Notification( config = item.config, 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 fbf1117370..862b6c770d 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 @@ -13,6 +13,7 @@ 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.TangemPayState.Progress +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @@ -41,7 +42,6 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.home_button_scan), onRefreshClick = {}, shouldShowProgress = false, @@ -49,13 +49,30 @@ private fun TangemPayMainScreenBlockPreview() { ), isBalanceHidden = false, ) + TangemPayMainScreenBlock( + state = TangemPayState.TemporaryUnavailable(WalletNotification.Warning.TangemPayUnreachable), + isBalanceHidden = false, + ) + TangemPayMainScreenBlock( + state = TangemPayState.OnboardingBanner(onClick = {}, closeOnClick = {}), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.FailedIssue( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_failed_to_issue_card), + iconRes = R.drawable.ic_alert_24, + onButtonClick = { }, + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - description = TextReference.EMPTY, + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_kyc_in_progress), buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), iconRes = R.drawable.ic_promo_kyc_36, onButtonClick = {}, @@ -65,19 +82,8 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.EMPTY, - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = {}, - ), - isBalanceHidden = false, - ) - - TangemPayMainScreenBlock( - Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.Res(R.string.tangempay_issue_card_notification_description), + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_issuing_your_card), buttonText = TextReference.EMPTY, iconRes = R.drawable.ic_tangem_pay_promo_card_36, onButtonClick = {}, 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 index b3c06f3a28..715a6168a2 100644 --- 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 @@ -67,7 +67,6 @@ private fun TangemPayRefreshBlockPreview() { TangemPayRefreshBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), onRefreshClick = {}, shouldShowProgress = true,