diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index df2755faef..1b9562b290 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api(projects.features.markets.api) api(projects.features.onramp.api) api(projects.features.sendV2.api) + api(projects.features.tokenRecieve.api) /* Data */ implementation(projects.data.common) @@ -39,6 +40,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.notifications.models) + implementation(projects.domain.transaction) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 0e4b9f5c67..438a815cc3 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.nft.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -34,6 +35,8 @@ dependencies { implementation(projects.domain.nft.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction) + implementation(projects.domain.tokens) /** Common */ implementation(projects.common.ui) diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 9653f895cb..d6abeb7356 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.features.manageTokens.api) implementation(projects.features.biometry.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -49,6 +50,7 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.onramp) + implementation(projects.domain.transaction) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt new file mode 100644 index 0000000000..c37becdbb2 --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.tokenreceive + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig + +interface TokenReceiveComponent : ComposableBottomSheetComponent { + + data class Params( + val config: TokenReceiveConfig, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index dfd0d93ada..b1b8d7b048 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) /** Feature Apis */ implementation(projects.features.tokenRecieve.api) diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt new file mode 100644 index 0000000000..291ad7b029 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -0,0 +1,197 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +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.navigation.inner.InnerRouter +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.TokenReceiveContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultTokenReceiveComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: TokenReceiveComponent.Params, +) : TokenReceiveComponent, AppComponentContext by appComponentContext { + + private val model: TokenReceiveModel = getOrCreateModel(params) + private val innerRouter = InnerRouter( + stackNavigation = model.stackNavigation, + popCallback = { onChildBack() }, + ) + + private val contentStack = childStack( + key = "TokenReceiveFlowStack", + source = model.stackNavigation, + serializer = TokenReceiveRoutes.serializer(), + initialConfiguration = getInitialConfig(model.params.config.shouldShowWarning), + handleBackButton = false, + childFactory = ::screenChild, + ) + + @Composable + override fun BottomSheet() { + val content by contentStack.subscribeAsState() + val currentRoute = content.active.configuration + + TokenReceiveContentSheet( + route = currentRoute, + onCloseClick = ::dismiss, + onBackClick = ::onChildBack, + contentStack = content, + ) + } + + override fun dismiss() { + model.params.onDismiss() + } + + private fun onChildBack() { + when (contentStack.value.active.configuration) { + is TokenReceiveRoutes.QrCode -> model.stackNavigation.pop() + TokenReceiveRoutes.ReceiveAssets, + TokenReceiveRoutes.Warning, + -> dismiss() + } + } + + private fun screenChild( + config: TokenReceiveRoutes, + componentContext: ComponentContext, + ): ComposableContentComponent { + val appComponentContext = childByContext( + componentContext = componentContext, + router = innerRouter, + ) + return when (config) { + is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent( + appComponentContext = appComponentContext, + params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams( + network = model.params.config.cryptoCurrency.network.name, + address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"), + callback = model, + onDismiss = ::dismiss, + id = config.addressId, + ), + ) + TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent( + appComponentContext = appComponentContext, + params = TokenReceiveAssetsComponent.TokenReceiveAssetsParams( + addresses = model.state.value.addresses, + callback = model, + onDismiss = ::dismiss, + notificationConfigs = model.state.value.notificationConfigs, + showMemoDisclaimer = model.params.config.showMemoDisclaimer, + fullName = model.params.config.cryptoCurrency.network.name, + ), + ) + TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent( + appComponentContext = appComponentContext, + params = TokenReceiveWarningComponent.TokenReceiveWarningParams( + iconState = model.state.value.iconState, + callback = model, + onDismiss = ::dismiss, + network = model.params.config.cryptoCurrency.network, + ), + ) + } + } + + private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes { + return if (shouldShowWarning) { + TokenReceiveRoutes.Warning + } else { + TokenReceiveRoutes.ReceiveAssets + } + } + + @AssistedFactory + interface Factory : TokenReceiveComponent.Factory { + override fun create( + context: AppComponentContext, + params: TokenReceiveComponent.Params, + ): DefaultTokenReceiveComponent + } +} + +@Composable +internal fun TokenReceiveContentSheet( + route: TokenReceiveRoutes, + onCloseClick: () -> Unit, + onBackClick: () -> Unit, + contentStack: ChildStack, +) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBackClick, + containerColor = TangemTheme.colors.background.tertiary, + title = { + Title( + route = route, + onBackClick = onBackClick, + onCloseClick = onCloseClick, + ) + }, + content = { + TokenReceiveContent( + stackState = contentStack, + modifier = Modifier, + ) + }, + ) +} + +@Composable +private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { + when (route) { + is TokenReceiveRoutes.QrCode -> { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.ReceiveAssets -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.domain_receive_assets_navigation_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.Warning -> { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + } +} + +internal interface TokenReceiveModelCallback : + TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback, + TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback, + TokenReceiveWarningComponent.TokenReceiveWarningModelCallback \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt new file mode 100644 index 0000000000..de8c7e2578 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal class TokenReceiveAssetsComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveAssetsParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveAssetsContent(assetsUM = state) + } + + internal interface TokenReceiveAssetsModelCallback { + fun onQrCodeClick(id: Int) + fun onCopyClick(id: Int) + } + + data class TokenReceiveAssetsParams( + val notificationConfigs: ImmutableList, + val addresses: ImmutableMap, + val callback: TokenReceiveAssetsModelCallback, + val onDismiss: () -> Unit, + val showMemoDisclaimer: Boolean, + val fullName: String, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt new file mode 100644 index 0000000000..8191f9a208 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent + +internal class TokenReceiveQrCodeComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveQrCodeParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveQrCodeModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveQrCodeContent(qrCodeUM = state) + } + + internal interface TokenReceiveQrCodeModelCallback { + fun onCopyClick(id: Int) + fun onShareClick(address: String) + } + + data class TokenReceiveQrCodeParams( + val id: Int, + val network: String, + val address: ReceiveAddress, + val callback: TokenReceiveQrCodeModelCallback, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt new file mode 100644 index 0000000000..46ce303258 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.network.Network +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +import com.tangem.features.tokenreceive.ui.TokenReceiveWarningContent + +internal class TokenReceiveWarningComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveWarningParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveWarningModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveWarningContent(warningUM = state) + } + + internal interface TokenReceiveWarningModelCallback { + fun onWarningAcknowledged() + } + + data class TokenReceiveWarningParams( + val iconState: CurrencyIconState, + val callback: TokenReceiveWarningModelCallback, + val onDismiss: () -> Unit, + val network: Network, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt new file mode 100644 index 0000000000..44cfdf6d8b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt @@ -0,0 +1,68 @@ +package com.tangem.features.tokenreceive.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.tokenreceive.DefaultTokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.component.DefaultTokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureToggleModule { + + @Provides + @Singleton + fun provideTokenReceiveFeatureToggle(featureTogglesManager: FeatureTogglesManager): TokenReceiveFeatureToggle { + return DefaultTokenReceiveFeatureToggle( + featureTogglesManager = featureTogglesManager, + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultTokenReceiveComponent.Factory): TokenReceiveComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(TokenReceiveModel::class) + fun bindsTokenReceiveModel(model: TokenReceiveModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveAssetsModel::class) + fun bindsTokenReceiveAssetsModel(model: TokenReceiveAssetsModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveQrCodeModel::class) + fun bindsTokenReceiveQrCodeModel(model: TokenReceiveQrCodeModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveWarningModel::class) + fun bindsTokenReceiveWarningModel(model: TokenReceiveWarningModel): Model +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt new file mode 100644 index 0000000000..04b0caf856 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent +import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveAssetsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + ReceiveAssetsUM( + onCopyClick = params.callback::onCopyClick, + onOpenQrCodeClick = params.callback::onQrCodeClick, + addresses = params.addresses, + showMemoDisclaimer = params.showMemoDisclaimer, + isEnsResultLoading = false, + notificationConfigs = params.notificationConfigs, + fullName = params.fullName, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt new file mode 100644 index 0000000000..3313bf1cb9 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt @@ -0,0 +1,186 @@ +package com.tangem.features.tokenreceive.model + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.push +import com.tangem.common.ui.notifications.NotificationUM +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.R +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.ens.EnsAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@ModelScoped +internal class TokenReceiveModel @Inject constructor( + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, + private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase, + private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model(), TokenReceiveModelCallback { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + val params = paramsContainer.require() + + val stackNavigation = StackNavigation() + + internal val state: StateFlow + field = MutableStateFlow(getInitState()) + + init { + modelScope.launch { + load() + } + } + + override fun onQrCodeClick(id: Int) { + stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id)) + } + + override fun onCopyClick(id: Int) { + val addressToCopy = state.value.addresses[id] ?: return + clipboardManager.setText(text = addressToCopy.value, isSensitive = true) + } + + override fun onShareClick(address: String) { + shareManager.shareText(text = address) + } + + override fun onWarningAcknowledged() { + modelScope.launch { + saveViewedTokenReceiveWarningUseCase.invoke( + when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.name + Asset.NFT -> asset.name + }, + ) + stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets) + } + } + + private fun mapAddresses(addresses: List): ImmutableMap { + return buildMap { + addresses.mapIndexed { index, model -> + val type = when (model.nameService) { + ReceiveAddressModel.NameService.Default -> { + ReceiveAddress.Type.Default( + displayName = stringReference(model.displayName), + ) + } + ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens + } + put( + key = index, + value = ReceiveAddress( + value = model.value, + type = type, + ), + ) + } + }.toPersistentMap() + } + + private suspend fun load() = withContext(dispatchers.default) { + state.value = state.value.copy( + isEnsResultLoading = true, + ) + + val reverseResolveResult = + if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) { + getReverseResolvedEnsAddressUseCase( + userWalletId = params.config.userWalletId, + network = params.config.cryptoCurrency.network, + addresses = params.config.receiveAddress.map { it.value }, + ) + } else { + emptyList() + } + + val currentAddressValues = state.value.addresses.values.map { it.value }.toSet() + + val newEnsAddresses = reverseResolveResult + .filterIsInstance() + .filterNot { it.name in currentAddressValues } + .map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) } + + val combinedAddresses = (state.value.addresses.values + newEnsAddresses) + .sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens }) + + val updatedAddresses = combinedAddresses + .mapIndexed { index, address -> index to address } + .toMap() + .toPersistentMap() + + state.value = state.value.copy( + isEnsResultLoading = false, + addresses = updatedAddresses, + ) + } + + private fun getNotifications(): List { + return buildList { + add( + NotificationUM.Info( + title = resourceReference( + R.string.receive_bottom_sheet_warning_title, + wrappedList( + when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.symbol + Asset.NFT -> asset.name + }, + params.config.cryptoCurrency.network.name, + ), + ), + subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), + iconTint = NotificationConfig.IconTint.Accent, + ), + ) + + params.config.tokenReceiveNotification.map { notification -> + add( + NotificationUM.Warning( + title = resourceReference(notification.title), + subtitle = resourceReference(notification.subtitle), + ), + ) + } + } + } + + private fun getInitState(): TokenReceiveUM { + return TokenReceiveUM( + addresses = mapAddresses(params.config.receiveAddress), + iconState = iconStateConverter.convert(params.config.cryptoCurrency), + network = params.config.cryptoCurrency.network.name, + isEnsResultLoading = false, + notificationConfigs = getNotifications().toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt new file mode 100644 index 0000000000..e7b1915b1b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.QrCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveQrCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + QrCodeUM( + network = params.network, + addressValue = params.address.value, + addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: stringReference(""), + onCopyClick = { params.callback.onCopyClick(params.id) }, + onShareClick = params.callback::onShareClick, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt new file mode 100644 index 0000000000..8c686c07a7 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.iconResId +import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent +import com.tangem.features.tokenreceive.ui.state.WarningUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveWarningModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + WarningUM( + iconState = params.iconState, + onWarningAcknowledged = params.callback::onWarningAcknowledged, + network = params.network.name, + networkIcon = params.network.iconResId, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt new file mode 100644 index 0000000000..c98a6e3c12 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tokenreceive.route + +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +internal sealed interface TokenReceiveRoutes : Route { + + @Serializable + data object Warning : TokenReceiveRoutes + + @Serializable + data object ReceiveAssets : TokenReceiveRoutes + + @Serializable + data class QrCode(val addressId: Int) : TokenReceiveRoutes +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt index 2eb6310cbd..d248c1b34d 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt @@ -60,7 +60,7 @@ internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) { Buttons( onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) }, - onCopyClick = { qrCodeUM.onCopyClick(qrCodeUM.addressValue) }, + onCopyClick = qrCodeUM.onCopyClick, snackbarHostState = snackbarHostState, ) } diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt index e3c4f5adf0..274735da2a 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.tokenreceive.ui.state import com.tangem.core.ui.extensions.TextReference internal data class QrCodeUM( - val onCopyClick: (String) -> Unit, + val onCopyClick: (Int) -> Unit, val onShareClick: (String) -> Unit, val addressName: TextReference, val addressValue: String, diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4bcd8ce768..5da95d55a3 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -101,5 +101,8 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.txhistory.api) implementation(projects.features.sendV2.api) + implementation(projects.features.tokenRecieve.api) + + implementation(deps.decompose.ext.compose) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 410b089abe..2291d82965 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -4,16 +4,24 @@ 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.router.slot.childSlot +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.txhistory.component.TxHistoryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Assisted params: TokenDetailsComponent.Params, tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory, txHistoryComponentFactory: TxHistoryComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { private val model: TokenDetailsModel = getOrCreateModel(params) @@ -37,6 +46,13 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + init { lifecycle.subscribe( onPause = model::onPause, @@ -54,12 +70,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NavigationBar3ButtonsScrim() TokenDetailsScreen( state = state, tokenMarketBlockComponent = tokenMarketBlockComponent, txHistoryComponent = txHistoryComponent, ) + bottomSheet.child?.instance?.BottomSheet() } private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? { @@ -68,6 +86,17 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( return TokenMarketBlockComponent.Params(cryptoCurrency = this) } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : TokenDetailsComponent.Factory { override fun create( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f5352430b4..7f2de4ef2c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable import androidx.paging.cachedIn import arrow.core.getOrElse import arrow.core.merge +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -35,8 +37,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -60,10 +65,7 @@ import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.IncompleteTransactionError import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.AssociateAssetUseCase -import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase -import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase -import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -82,6 +84,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -137,6 +140,9 @@ internal class TokenDetailsModel @Inject constructor( private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val sendFeatureToggles: SendFeatureToggles, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model(), TokenDetailsClickIntents { private val params = paramsContainer.require() @@ -159,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor( /** Transaction id to check for status */ private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), @@ -594,20 +602,27 @@ internal class TokenDetailsModel @Inject constructor( } modelScope.launch { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) - - internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( - currency = cryptoCurrency, - networkAddress = networkAddress, - onCopyClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) - shareManager.shareText(text = it) - }, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses(addresses = networkAddress), + ) + } else { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) + internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( + currency = cryptoCurrency, + networkAddress = networkAddress, + onCopyClick = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + analyticsEventsHandler.send( + TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), + ) + shareManager.shareText(text = it) + }, + ) + } } } @@ -1061,6 +1076,43 @@ internal class TokenDetailsModel @Inject constructor( .launchIn(modelScope) } + private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig { + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e6d1f16109..a71f5825e3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) implementation(projects.domain.notifications.toggles) + implementation(projects.domain.transaction) /** Feature Apis */ implementation(projects.features.details.api) @@ -113,6 +114,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.tokenRecieve.api) /** Common modules */ implementation(projects.common)