diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e644cefa16..c0d7a0dfb5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -115,10 +115,17 @@ Enter address Invalid address Keep editing + You can not create more than 20 addresses. Delete one to add new. + Can\'t add new address + Contact name is required + Contact name contains invalid characters + Contact name must not exceed 50 characters + That name is already taken on this wallet New contact No contacts yet Contacts added will appear here Remove address + Save to Wallet This contact will be linked to this wallet’s address book. Select network Address book @@ -352,6 +359,7 @@ Get token Go to provider Go to token + Go to verification Got it Hide Hold to %s @@ -732,6 +740,8 @@ Key Generation All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering. Hardware-Level Security + Network activity is high. You can continue now or try again later when fees may be lower. + Network fee is higher than usual Add Existing Wallet Create New Wallet Order Tangem @@ -1402,6 +1412,7 @@ Memo Check your network connection Network fee info unreachable + from %1$s in %2$s You send From %s Gas limit diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt index d40b5d6c0a..072a256769 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt @@ -16,6 +16,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayEligibilityManager 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.wallets.usecase.GenerateBuyTangemCardLinkUseCase 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.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.info.AppInfoProvider -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.slot -import io.mockk.unmockkObject +import io.mockk.* import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -66,6 +63,7 @@ internal abstract class DetailsModelTestBase { protected val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase = mockk() protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) protected val tangemPayEligibilityManager: TangemPayEligibilityManager = mockk() + protected val getVirtualAccountEligibilityUseCase: GetVirtualAccountEligibilityUseCase = mockk() // Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven. protected val wcSlot = slot() @@ -89,6 +87,7 @@ internal abstract class DetailsModelTestBase { every { appInfoProvider.appVersion } returns "1.2.3" every { appInfoProvider.appVersionCode } returns 456 coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList() + coEvery { getVirtualAccountEligibilityUseCase(any()) } returns VirtualAccountEligibility.NotAvailable every { itemsBuilder.buildAll( @@ -128,6 +127,7 @@ internal abstract class DetailsModelTestBase { generateBuyTangemCardLinkUseCase = generateBuyTangemCardLinkUseCase, analyticsEventHandler = analyticsEventHandler, tangemPayEligibilityManager = tangemPayEligibilityManager, + getVirtualAccountEligibilityUseCase = getVirtualAccountEligibilityUseCase, ) protected fun stubBuildAllReturns(list: ImmutableList) { diff --git a/features/virtual-accounts/details/impl/build.gradle.kts b/features/virtual-accounts/details/impl/build.gradle.kts index 1d9448064a..0993a9f074 100644 --- a/features/virtual-accounts/details/impl/build.gradle.kts +++ b/features/virtual-accounts/details/impl/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) id("configuration") @@ -14,12 +15,15 @@ dependencies { /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.res) implementation(projects.core.ui) implementation(projects.core.utils) /** Domain */ implementation(projects.domain.models) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) /** Features */ implementation(projects.features.virtualAccounts.details.api) @@ -31,6 +35,10 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.decompose.ext.compose) + /** Other */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt index 7ad7ef7c21..50ce82e412 100644 --- a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt @@ -4,24 +4,56 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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.childByContext 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.main.addfunds.VirtualAccountAddFundsBottomSheetComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject internal class DefaultVirtualAccountMainComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: VirtualAccountMainComponent.Params, + @Assisted private val params: VirtualAccountMainComponent.Params, ) : VirtualAccountMainComponent, AppComponentContext by appComponentContext { private val model: VirtualAccountMainModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = VirtualAccountMainNavigationBottomSheetConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() 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 diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt index 65a9fd1e60..a05c327e16 100644 --- a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt @@ -1,6 +1,9 @@ package com.tangem.features.virtualaccount.main 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.model.Model 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.features.virtualaccount.details.component.VirtualAccountMainComponent 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,16 +25,22 @@ internal class VirtualAccountMainModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, -) : Model() { +) : Model(), VirtualAccountAddFundsListener { @Suppress("UnusedPrivateProperty") private val params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow field = MutableStateFlow( createInitialState(), ) + override fun onAddFundsDismiss() { + bottomSheetNavigation.dismiss() + } + private fun createInitialState(): VirtualAccountMainUM = VirtualAccountMainUM( title = resourceReference(R.string.virtual_account_title), subtitle = resourceReference(R.string.tangempay_usdc_on_polygon_network), @@ -40,7 +51,63 @@ internal class VirtualAccountMainModel @Inject constructor( isBalanceHidden = false, onBackClick = { router.pop() }, onMenuClick = {}, - onAddFundsClick = {}, + onAddFundsClick = ::onAddFundsClick, 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, + ) } \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainNavigationBottomSheetConfig.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainNavigationBottomSheetConfig.kt new file mode 100644 index 0000000000..f0de59ec19 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainNavigationBottomSheetConfig.kt @@ -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, + val dailyDepositLimit: String, + ) : VirtualAccountMainNavigationBottomSheetConfig +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheet.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheet.kt new file mode 100644 index 0000000000..4537376d28 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheet.kt @@ -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( + 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), + ) + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheetComponent.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheetComponent.kt new file mode 100644 index 0000000000..4faaffcba5 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsBottomSheetComponent.kt @@ -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, + val dailyDepositLimit: String, + val listener: VirtualAccountAddFundsListener, + ) + + data class RequisitesRow( + val title: TextReference, + val titleForShare: String, + val value: String, + ) +} + +internal interface VirtualAccountAddFundsListener { + fun onAddFundsDismiss() +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsModel.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsModel.kt new file mode 100644 index 0000000000..d78ef65a65 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsModel.kt @@ -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() + + val uiState: StateFlow + 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}") + } + } + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsUM.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsUM.kt new file mode 100644 index 0000000000..4665eddc8f --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/addfunds/VirtualAccountAddFundsUM.kt @@ -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, + val dailyLimit: String, + val onShareClick: () -> Unit, + ) : Content + } + + @Immutable + data class DetailItem( + val label: TextReference, + val value: String, + val onCopyClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt index 9c621bc6fc..4b85e1f7d4 100644 --- a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.virtualaccount.main.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.virtualaccount.main.VirtualAccountMainModel +import com.tangem.features.virtualaccount.main.addfunds.VirtualAccountAddFundsModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -17,4 +18,9 @@ internal interface VirtualAccountMainModelModule { @IntoMap @ClassKey(VirtualAccountMainModel::class) fun bindVirtualAccountMainModel(model: VirtualAccountMainModel): Model + + @Binds + @IntoMap + @ClassKey(VirtualAccountAddFundsModel::class) + fun bindVirtualAccountAddFundsModel(model: VirtualAccountAddFundsModel): Model } \ No newline at end of file