Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-26 04:31:35 -07:00
parent 0c7586106e
commit 94ded5dcca
11 changed files with 621 additions and 9 deletions

View file

@ -115,10 +115,17 @@
<string name="address_book_enter_address">Enter address</string> <string name="address_book_enter_address">Enter address</string>
<string name="address_book_invalid_address_error">Invalid address</string> <string name="address_book_invalid_address_error">Invalid address</string>
<string name="address_book_keep_editing">Keep editing</string> <string name="address_book_keep_editing">Keep editing</string>
<string name="address_book_max_networks_alert_description">You can not create more than 20 addresses. Delete one to add new.</string>
<string name="address_book_max_networks_alert_title">Can\'t add new address </string>
<string name="address_book_name_empty_error">Contact name is required</string>
<string name="address_book_name_invalid_chars_error">Contact name contains invalid characters</string>
<string name="address_book_name_max_chars_error">Contact name must not exceed 50 characters</string>
<string name="address_book_name_taken_error">That name is already taken on this wallet</string>
<string name="address_book_new_contact">New contact</string> <string name="address_book_new_contact">New contact</string>
<string name="address_book_no_contacts">No contacts yet</string> <string name="address_book_no_contacts">No contacts yet</string>
<string name="address_book_no_contacts_description">Contacts added will appear here</string> <string name="address_book_no_contacts_description">Contacts added will appear here</string>
<string name="address_book_remove_address">Remove address</string> <string name="address_book_remove_address">Remove address</string>
<string name="address_book_save_to_wallet_title">Save to Wallet</string>
<string name="address_book_save_wallet_to_description">This contact will be linked to this wallets address book.</string> <string name="address_book_save_wallet_to_description">This contact will be linked to this wallets address book.</string>
<string name="address_book_select_network">Select network</string> <string name="address_book_select_network">Select network</string>
<string name="address_book_title">Address book</string> <string name="address_book_title">Address book</string>
@ -352,6 +359,7 @@
<string name="common_get_token">Get token</string> <string name="common_get_token">Get token</string>
<string name="common_go_to_provider">Go to provider</string> <string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string> <string name="common_go_to_token">Go to token</string>
<string name="common_go_to_verification">Go to verification</string>
<string name="common_got_it">Got it</string> <string name="common_got_it">Got it</string>
<string name="common_hide">Hide</string> <string name="common_hide">Hide</string>
<string name="common_hold_to">Hold to %s</string> <string name="common_hold_to">Hold to %s</string>
@ -732,6 +740,8 @@
<string name="hardware_wallet_key_feature_title">Key Generation</string> <string name="hardware_wallet_key_feature_title">Key Generation</string>
<string name="hardware_wallet_security_feature_description">All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering.</string> <string name="hardware_wallet_security_feature_description">All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering.</string>
<string name="hardware_wallet_security_feature_title">Hardware-Level Security</string> <string name="hardware_wallet_security_feature_title">Hardware-Level Security</string>
<string name="high_fee_warning_description">Network activity is high. You can continue now or try again later when fees may be lower.</string>
<string name="high_fee_warning_title">Network fee is higher than usual</string>
<string name="home_button_add_existing_wallet">Add Existing Wallet</string> <string name="home_button_add_existing_wallet">Add Existing Wallet</string>
<string name="home_button_create_new_wallet">Create New Wallet</string> <string name="home_button_create_new_wallet">Create New Wallet</string>
<string name="home_button_order">Order Tangem</string> <string name="home_button_order">Order Tangem</string>
@ -1402,6 +1412,7 @@
<string name="send_extras_hint_memo">Memo</string> <string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string> <string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string> <string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_account_in_wallet_name">from %1$s in %2$s</string>
<string name="send_from_title">You send</string> <string name="send_from_title">You send</string>
<string name="send_from_wallet_name">From %s</string> <string name="send_from_wallet_name">From %s</string>
<string name="send_gas_limit">Gas limit</string> <string name="send_gas_limit">Gas limit</string>

View file

@ -16,6 +16,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.virtualaccount.model.VirtualAccountEligibility
import com.tangem.domain.virtualaccount.usecase.GetVirtualAccountEligibilityUseCase
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -27,12 +29,7 @@ import com.tangem.features.details.utils.ItemsBuilder
import com.tangem.features.details.utils.SocialsBuilder import com.tangem.features.details.utils.SocialsBuilder
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.info.AppInfoProvider
import io.mockk.coEvery import io.mockk.*
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.unmockkObject
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -66,6 +63,7 @@ internal abstract class DetailsModelTestBase {
protected val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase = mockk() protected val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase = mockk()
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true)
protected val tangemPayEligibilityManager: TangemPayEligibilityManager = mockk() protected val tangemPayEligibilityManager: TangemPayEligibilityManager = mockk()
protected val getVirtualAccountEligibilityUseCase: GetVirtualAccountEligibilityUseCase = mockk()
// Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven. // Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven.
protected val wcSlot = slot<Boolean>() protected val wcSlot = slot<Boolean>()
@ -89,6 +87,7 @@ internal abstract class DetailsModelTestBase {
every { appInfoProvider.appVersion } returns "1.2.3" every { appInfoProvider.appVersion } returns "1.2.3"
every { appInfoProvider.appVersionCode } returns 456 every { appInfoProvider.appVersionCode } returns 456
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList() coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList()
coEvery { getVirtualAccountEligibilityUseCase(any()) } returns VirtualAccountEligibility.NotAvailable
every { every {
itemsBuilder.buildAll( itemsBuilder.buildAll(
@ -128,6 +127,7 @@ internal abstract class DetailsModelTestBase {
generateBuyTangemCardLinkUseCase = generateBuyTangemCardLinkUseCase, generateBuyTangemCardLinkUseCase = generateBuyTangemCardLinkUseCase,
analyticsEventHandler = analyticsEventHandler, analyticsEventHandler = analyticsEventHandler,
tangemPayEligibilityManager = tangemPayEligibilityManager, tangemPayEligibilityManager = tangemPayEligibilityManager,
getVirtualAccountEligibilityUseCase = getVirtualAccountEligibilityUseCase,
) )
protected fun stubBuildAllReturns(list: ImmutableList<DetailsItemUM>) { protected fun stubBuildAllReturns(list: ImmutableList<DetailsItemUM>) {

View file

@ -1,6 +1,7 @@
plugins { plugins {
alias(deps.plugins.android.library) alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.kapt) alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android) alias(deps.plugins.hilt.android)
id("configuration") id("configuration")
@ -14,12 +15,15 @@ dependencies {
/** Core */ /** Core */
implementation(projects.core.configToggles) implementation(projects.core.configToggles)
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.res) implementation(projects.core.res)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.utils) implementation(projects.core.utils)
/** Domain */ /** Domain */
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
/** Features */ /** Features */
implementation(projects.features.virtualAccounts.details.api) implementation(projects.features.virtualAccounts.details.api)
@ -31,6 +35,10 @@ dependencies {
implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.tooling)
implementation(deps.decompose.ext.compose) implementation(deps.decompose.ext.compose)
/** Other */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
/** DI */ /** DI */
implementation(deps.hilt.android) implementation(deps.hilt.android)
kapt(deps.hilt.kapt) kapt(deps.hilt.kapt)

View file

@ -4,24 +4,56 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent
import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsBottomSheetComponent
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
internal class DefaultVirtualAccountMainComponent @AssistedInject constructor( internal class DefaultVirtualAccountMainComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted params: VirtualAccountMainComponent.Params, @Assisted private val params: VirtualAccountMainComponent.Params,
) : VirtualAccountMainComponent, AppComponentContext by appComponentContext { ) : VirtualAccountMainComponent, AppComponentContext by appComponentContext {
private val model: VirtualAccountMainModel = getOrCreateModel(params = params) private val model: VirtualAccountMainModel = getOrCreateModel(params = params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = VirtualAccountMainNavigationBottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable @Composable
override fun Content(modifier: Modifier) { override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle() val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
VirtualAccountMainScreen(state = state, modifier = modifier) VirtualAccountMainScreen(state = state, modifier = modifier)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: VirtualAccountMainNavigationBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
return when (config) {
is VirtualAccountMainNavigationBottomSheetConfig.AddFunds -> VirtualAccountAddFundsBottomSheetComponent(
appComponentContext = childByContext(componentContext),
params = VirtualAccountAddFundsBottomSheetComponent.Params(
userWalletId = params.userWalletId,
listener = model,
requisites = config.requisites,
dailyDepositLimit = config.dailyDepositLimit,
),
)
}
} }
@AssistedFactory @AssistedFactory

View file

@ -1,6 +1,9 @@
package com.tangem.features.virtualaccount.main package com.tangem.features.virtualaccount.main
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
@ -9,6 +12,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent
import com.tangem.features.virtualaccount.details.impl.R import com.tangem.features.virtualaccount.details.impl.R
import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsBottomSheetComponent
import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsListener
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -20,16 +25,22 @@ internal class VirtualAccountMainModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val router: Router, private val router: Router,
) : Model() { ) : Model(), VirtualAccountAddFundsListener {
@Suppress("UnusedPrivateProperty") @Suppress("UnusedPrivateProperty")
private val params = paramsContainer.require<VirtualAccountMainComponent.Params>() private val params = paramsContainer.require<VirtualAccountMainComponent.Params>()
val bottomSheetNavigation: SlotNavigation<VirtualAccountMainNavigationBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<VirtualAccountMainUM> val uiState: StateFlow<VirtualAccountMainUM>
field = MutableStateFlow( field = MutableStateFlow(
createInitialState(), createInitialState(),
) )
override fun onAddFundsDismiss() {
bottomSheetNavigation.dismiss()
}
private fun createInitialState(): VirtualAccountMainUM = VirtualAccountMainUM( private fun createInitialState(): VirtualAccountMainUM = VirtualAccountMainUM(
title = resourceReference(R.string.virtual_account_title), title = resourceReference(R.string.virtual_account_title),
subtitle = resourceReference(R.string.tangempay_usdc_on_polygon_network), subtitle = resourceReference(R.string.tangempay_usdc_on_polygon_network),
@ -40,7 +51,63 @@ internal class VirtualAccountMainModel @Inject constructor(
isBalanceHidden = false, isBalanceHidden = false,
onBackClick = { router.pop() }, onBackClick = { router.pop() },
onMenuClick = {}, onMenuClick = {},
onAddFundsClick = {}, onAddFundsClick = ::onAddFundsClick,
onSendClick = {}, onSendClick = {},
) )
private fun buildRequisites(details: VirtualAccountDepositDetails) = listOf(
VirtualAccountAddFundsBottomSheetComponent.RequisitesRow(
title = stringReference("Beneficiary name and address"),
titleForShare = "Beneficiary name and address",
value = "${details.beneficiaryName}\n${details.beneficiaryAddress}",
),
VirtualAccountAddFundsBottomSheetComponent.RequisitesRow(
title = stringReference("Bank name and address"),
titleForShare = "Bank name and address",
value = "${details.bankName}\n${details.bankAddress}",
),
VirtualAccountAddFundsBottomSheetComponent.RequisitesRow(
title = stringReference("Account number"),
titleForShare = "Account number",
value = details.accountNumber,
),
VirtualAccountAddFundsBottomSheetComponent.RequisitesRow(
title = stringReference("Routing number"),
titleForShare = "Routing number",
value = details.routingNumber,
),
)
private fun onAddFundsClick() {
val details = getDepositDetails()
bottomSheetNavigation.activate(
VirtualAccountMainNavigationBottomSheetConfig.AddFunds(
requisites = buildRequisites(details),
dailyDepositLimit = details.dailyDepositLimit,
),
)
}
// TODO v_rodionov: HARDCODE - get this data from backend
private fun getDepositDetails(): VirtualAccountDepositDetails {
return VirtualAccountDepositDetails(
beneficiaryName = "Ivan Ivanov",
beneficiaryAddress = "18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
bankName = "SSB Bank",
bankAddress = "8700 Perry Highway, Pittsburgh, PA 15237, US",
accountNumber = "707613210122",
routingNumber = "043087080",
dailyDepositLimit = "$10,000",
)
}
private data class VirtualAccountDepositDetails(
val beneficiaryName: String,
val beneficiaryAddress: String,
val bankName: String,
val bankAddress: String,
val accountNumber: String,
val routingNumber: String,
val dailyDepositLimit: String,
)
} }

View file

@ -0,0 +1,12 @@
package com.tangem.features.virtualaccount.main
import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsBottomSheetComponent.RequisitesRow
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface VirtualAccountMainNavigationBottomSheetConfig {
data class AddFunds(
val requisites: List<RequisitesRow>,
val dailyDepositLimit: String,
) : VirtualAccountMainNavigationBottomSheetConfig
}

View file

@ -0,0 +1,328 @@
package com.tangem.features.virtualaccount.main.addfunds
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.row.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_copy_24
import com.tangem.core.ui.res.generated.icons.ic_info_24
import com.tangem.core.ui.res.generated.icons.ic_sign_usd_32
import com.tangem.features.virtualaccount.details.impl.R
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.R as CoreUiR
@Composable
internal fun VirtualAccountAddFundsBottomSheet(state: VirtualAccountAddFundsUM) {
val title = stringReference("Account details")
.takeIf { state.content is VirtualAccountAddFundsUM.Content.Details }
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
title = title,
endContent = { TangemButton.Close(onClick = state.onDismiss) },
)
},
content = { _ ->
when (val content = state.content) {
is VirtualAccountAddFundsUM.Content.Intro -> IntroContent(content)
is VirtualAccountAddFundsUM.Content.Details -> DetailsContent(content)
}
},
)
}
@Composable
private fun IntroContent(content: VirtualAccountAddFundsUM.Content.Intro, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4),
horizontalAlignment = Alignment.CenterHorizontally,
) {
IntroIcons(modifier = Modifier.padding(top = TangemTheme.dimens2.x4))
TitleText(
text = stringReference("Received USD will be converted to USDC by 1:1 rate"),
modifier = Modifier.padding(top = TangemTheme.dimens2.x8),
)
SubtitleText(
text = stringReference("It might take 1-3 days to receive the money"),
modifier = Modifier.padding(top = TangemTheme.dimens2.x2),
)
InfoNotification(
title = stringReference("Only ACH and domestic wire transfers are available"),
subtitle = stringReference("SWIFT won't pass"),
modifier = Modifier.padding(top = TangemTheme.dimens2.x6),
)
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x4),
text = stringReference("Show details"),
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X12,
onClick = content.onShowDetailsClick,
)
}
}
@Composable
private fun DetailsContent(content: VirtualAccountAddFundsUM.Content.Details, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens2.x4),
) {
content.items.forEachIndexed { index, item ->
CopyableRow(
item = item,
divider = index != content.items.lastIndex,
)
}
InfoNotification(
title = stringReference("Available to deposit per day: ${content.dailyLimit}"),
subtitle = stringReference("Limit is resetting every day"),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(top = TangemTheme.dimens2.x3),
)
TangemButton(
text = resourceReference(R.string.common_share),
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X12,
onClick = content.onShareClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(top = TangemTheme.dimens2.x4),
)
}
}
@Composable
private fun CopyableRow(item: VirtualAccountAddFundsUM.DetailItem, divider: Boolean, modifier: Modifier = Modifier) {
TangemRow(
modifier = modifier,
divider = divider,
contentLead = TangemRowContentLead.Start,
verticalAlignment = TangemRowVerticalAlignment.Center,
titleSlot = {
TangemRowText(
text = item.label,
role = TangemRowTextRole.Subtitle,
)
},
subtitleSlot = {
TangemRowText(
text = item.value,
role = TangemRowTextRole.Title,
maxLines = Int.MAX_VALUE,
)
},
endSlot = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_copy_24),
onClick = item.onCopyClick,
size = TangemButton.Size.X9,
variant = TangemButton.Variant.Ghost,
contentDescription = item.label.resolveReference(),
)
},
)
}
@Composable
private fun InfoNotification(title: TextReference, subtitle: TextReference, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens2.x4))
.background(TangemTheme.colors3.bg.status.infoSubtle)
.padding(TangemTheme.dimens2.x4),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
imageVector = Icons.ic_info_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.status.info,
)
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
}
@Composable
private fun IntroIcons(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(-TangemTheme.dimens2.x4),
) {
UsdIcon()
UsdcIcon()
}
}
@Composable
private fun UsdIcon(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.opaque.primary)
.border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x8),
imageVector = Icons.ic_sign_usd_32,
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
}
}
@Composable
private fun UsdcIcon(modifier: Modifier = Modifier) {
Box(modifier = modifier.size(TangemTheme.dimens2.x20)) {
Image(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = CircleShape),
painter = painterResource(CoreUiR.drawable.img_usdc_16),
contentDescription = null,
)
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens2.x6)
.background(color = TangemTheme.colors3.bg.accent.violet, shape = CircleShape)
.border(
width = TangemTheme.dimens2.x0_5,
color = TangemTheme.colors3.bg.secondary,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
painter = painterResource(CoreUiR.drawable.ic_polygon_22),
contentDescription = null,
tint = TangemTheme.colors3.icon.inverse,
)
}
}
}
@Composable
private fun TitleText(text: TextReference, modifier: Modifier = Modifier) {
Text(
modifier = modifier.fillMaxWidth(),
text = text.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun SubtitleText(text: TextReference, modifier: Modifier = Modifier) {
Text(
modifier = modifier.fillMaxWidth(),
text = text.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun VirtualAccountAddFundsIntroPreview() {
TangemThemePreviewRedesign {
IntroContent(
content = VirtualAccountAddFundsUM.Content.Intro(
onShowDetailsClick = {},
),
modifier = Modifier.background(TangemTheme.colors3.bg.secondary),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun VirtualAccountAddFundsDetailsPreview() {
TangemThemePreviewRedesign {
DetailsContent(
content = VirtualAccountAddFundsUM.Content.Details(
items = persistentListOf(
VirtualAccountAddFundsUM.DetailItem(
label = stringReference("Beneficiary name and address"),
value = "Ivan Ivanov\n18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
onCopyClick = {},
),
VirtualAccountAddFundsUM.DetailItem(
label = stringReference("Account number"),
value = "707613210122",
onCopyClick = {},
),
),
dailyLimit = "$10,000",
onShareClick = {},
),
modifier = Modifier.background(TangemTheme.colors3.bg.secondary),
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.virtualaccount.main.addfunds
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.wallet.UserWalletId
internal class VirtualAccountAddFundsBottomSheetComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: VirtualAccountAddFundsModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
VirtualAccountAddFundsBottomSheet(state = state)
}
data class Params(
val userWalletId: UserWalletId,
val requisites: List<RequisitesRow>,
val dailyDepositLimit: String,
val listener: VirtualAccountAddFundsListener,
)
data class RequisitesRow(
val title: TextReference,
val titleForShare: String,
val value: String,
)
}
internal interface VirtualAccountAddFundsListener {
fun onAddFundsDismiss()
}

View file

@ -0,0 +1,70 @@
package com.tangem.features.virtualaccount.main.addfunds
import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastForEach
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@Stable
@ModelScoped
internal class VirtualAccountAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val clipboardManager: ClipboardManager,
private val shareManager: ShareManager,
) : Model() {
private val params = paramsContainer.require<VirtualAccountAddFundsBottomSheetComponent.Params>()
val uiState: StateFlow<VirtualAccountAddFundsUM>
field = MutableStateFlow(
VirtualAccountAddFundsUM(
onDismiss = ::onDismiss,
content = VirtualAccountAddFundsUM.Content.Intro(
onShowDetailsClick = { showDetailsContent() },
),
),
)
fun onDismiss() {
params.listener.onAddFundsDismiss()
}
private fun showDetailsContent() {
uiState.update { state ->
state.copy(
content = VirtualAccountAddFundsUM.Content.Details(
items = params.requisites
.map { detailItem(label = it.title, value = it.value) }
.toImmutableList(),
dailyLimit = params.dailyDepositLimit,
onShareClick = { shareManager.shareText(buildShareText()) },
),
)
}
}
private fun detailItem(label: TextReference, value: String) = VirtualAccountAddFundsUM.DetailItem(
label = label,
value = value,
onCopyClick = { clipboardManager.setText(text = value, isSensitive = true) },
)
private fun buildShareText(): String {
return buildString {
params.requisites.fastForEach { item ->
appendLine("${item.titleForShare}: ${item.value}")
}
}
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.virtualaccount.main.addfunds
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class VirtualAccountAddFundsUM(
val onDismiss: () -> Unit,
val content: Content,
) {
@Immutable
sealed interface Content {
data class Intro(
val onShowDetailsClick: () -> Unit,
) : Content
data class Details(
val items: ImmutableList<DetailItem>,
val dailyLimit: String,
val onShareClick: () -> Unit,
) : Content
}
@Immutable
data class DetailItem(
val label: TextReference,
val value: String,
val onCopyClick: () -> Unit,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.virtualaccount.main.di
import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.features.virtualaccount.main.VirtualAccountMainModel import com.tangem.features.virtualaccount.main.VirtualAccountMainModel
import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -17,4 +18,9 @@ internal interface VirtualAccountMainModelModule {
@IntoMap @IntoMap
@ClassKey(VirtualAccountMainModel::class) @ClassKey(VirtualAccountMainModel::class)
fun bindVirtualAccountMainModel(model: VirtualAccountMainModel): Model fun bindVirtualAccountMainModel(model: VirtualAccountMainModel): Model
@Binds
@IntoMap
@ClassKey(VirtualAccountAddFundsModel::class)
fun bindVirtualAccountAddFundsModel(model: VirtualAccountAddFundsModel): Model
} }