Updated on 2026-08-14
This commit is contained in:
commit
e1b8a026cb
79 changed files with 2712 additions and 43 deletions
|
|
@ -243,6 +243,8 @@ dependencies {
|
|||
implementation(projects.features.tangempay.details.impl)
|
||||
implementation(projects.features.tangempay.main.api)
|
||||
implementation(projects.features.tangempay.main.impl)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
implementation(projects.features.tokenRecieve.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -154,4 +154,10 @@ internal object NFTDomainModule {
|
|||
): ObserveAndClearNFTCacheIfNeedUseCase {
|
||||
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase {
|
||||
return GetNFTCurrencyUseCase(nftRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
|
||||
|
|
@ -469,4 +470,20 @@ internal object TokensDomainModule {
|
|||
fun provideGetAssetRequirementsUseCase(walletManagersFacade: WalletManagersFacade): GetAssetRequirementsUseCase {
|
||||
return GetAssetRequirementsUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetViewedTokenReceiveWarningUseCase(
|
||||
tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
|
||||
): GetViewedTokenReceiveWarningUseCase {
|
||||
return GetViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSaveViewedTokenReceiveWarningUseCase(
|
||||
tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
|
||||
): SaveViewedTokenReceiveWarningUseCase {
|
||||
return SaveViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.tokens.TokensFeatureToggles
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import dagger.Module
|
||||
|
|
@ -220,4 +221,24 @@ internal object TransactionDomainModule {
|
|||
): CreateNFTTransferTransactionUseCase {
|
||||
return CreateNFTTransferTransactionUseCase(transactionRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetEnsNameUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
): GetEnsNameUseCase {
|
||||
return GetEnsNameUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
walletAddressServiceRepository = walletAddressServiceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetReverseResolvedEnsAddressUseCase(
|
||||
walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
): GetReverseResolvedEnsAddressUseCase {
|
||||
return GetReverseResolvedEnsAddressUseCase(walletAddressServiceRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -273,6 +273,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
|
|||
iconResId: Int = R.drawable.ic_alert_circle_24,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
iconTint: NotificationConfig.IconTint = NotificationConfig.IconTint.Unspecified,
|
||||
) : NotificationUM(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
|
|
@ -280,6 +281,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
|
|||
iconResId = iconResId,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
iconTint = iconTint,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,10 @@ sealed class AnalyticsParam {
|
|||
Pending(value = "Pending"),
|
||||
}
|
||||
|
||||
enum class EnsStatus(val value: String) {
|
||||
EMPTY("Empty"), FULL("Full")
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "Blockchain"
|
||||
const val TOKEN_PARAM = "Token"
|
||||
|
|
@ -240,5 +244,7 @@ sealed class AnalyticsParam {
|
|||
const val SEND_BLOCKCHAIN = "Send Blockchain"
|
||||
const val RECEIVE_BLOCKCHAIN = "Receive Blockchain"
|
||||
const val CHOSEN_TOKEN = "Token Chosen"
|
||||
const val ENS = "ENS"
|
||||
const val ENS_ADDRESS = "ENS Address"
|
||||
}
|
||||
}
|
||||
|
|
@ -58,5 +58,9 @@
|
|||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
"version": "5.28.0"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.token.DefaultTokenReceiveWarningActionStore
|
||||
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object TokenReceiveWarningModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenReceiveWarningStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): TokenReceiveWarningActionStore {
|
||||
return DefaultTokenReceiveWarningActionStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = setTypes<String>(),
|
||||
defaultValue = emptySet(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
internal class DefaultTokenReceiveWarningActionStore(
|
||||
private val persistenceStore: DataStore<Set<String>>,
|
||||
) : TokenReceiveWarningActionStore {
|
||||
|
||||
override suspend fun getSync(): Set<String> {
|
||||
return persistenceStore.data.firstOrNull() ?: emptySet()
|
||||
}
|
||||
|
||||
override suspend fun store(symbol: String) {
|
||||
persistenceStore.updateData { data -> data.plus(symbol) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
interface TokenReceiveWarningActionStore {
|
||||
|
||||
suspend fun getSync(): Set<String>
|
||||
|
||||
suspend fun store(symbol: String)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -28,11 +29,16 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
|
|||
* @param shouldDisplayNetwork specifies whether to display network badge
|
||||
*/
|
||||
@Composable
|
||||
fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) {
|
||||
fun CurrencyIcon(
|
||||
state: CurrencyIconState,
|
||||
modifier: Modifier = Modifier,
|
||||
shouldDisplayNetwork: Boolean = true,
|
||||
iconSize: Dp = 36.dp,
|
||||
) {
|
||||
BaseContainer(modifier = modifier) {
|
||||
val iconModifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.size(iconSize)
|
||||
|
||||
when (state) {
|
||||
is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.core.ui.components.currency.icon
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.getTintForTokenIcon
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
object CurrencyIconStateBuilder {
|
||||
|
||||
fun build(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = true,
|
||||
): CurrencyIconState = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> fromCoin(cryptoCurrency, isGrayscale, showCustomBadge)
|
||||
is CryptoCurrency.Token -> fromToken(cryptoCurrency, isGrayscale, showCustomBadge)
|
||||
}
|
||||
|
||||
private fun createCoinIcon(
|
||||
url: String? = null,
|
||||
@DrawableRes fallbackResId: Int = R.drawable.ic_empty_64,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = false,
|
||||
): CurrencyIconState.CoinIcon = CurrencyIconState.CoinIcon(
|
||||
url = url,
|
||||
fallbackResId = fallbackResId,
|
||||
isGrayscale = isGrayscale,
|
||||
showCustomBadge = showCustomBadge,
|
||||
)
|
||||
|
||||
private fun createTokenIcon(
|
||||
url: String? = null,
|
||||
@DrawableRes topBadgeIconResId: Int? = null,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = false,
|
||||
fallbackTint: Color = Color.Black,
|
||||
fallbackBackground: Color = Color.White,
|
||||
): CurrencyIconState.TokenIcon = CurrencyIconState.TokenIcon(
|
||||
url = url,
|
||||
topBadgeIconResId = topBadgeIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
fallbackTint = fallbackTint,
|
||||
fallbackBackground = fallbackBackground,
|
||||
showCustomBadge = showCustomBadge,
|
||||
)
|
||||
|
||||
private fun createCustomTokenIcon(
|
||||
tint: Color,
|
||||
background: Color,
|
||||
@DrawableRes topBadgeIconResId: Int,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = true,
|
||||
): CurrencyIconState.CustomTokenIcon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = tint,
|
||||
background = background,
|
||||
topBadgeIconResId = topBadgeIconResId,
|
||||
isGrayscale = isGrayscale,
|
||||
showCustomBadge = showCustomBadge,
|
||||
)
|
||||
|
||||
private fun fromCoin(
|
||||
coin: CryptoCurrency.Coin,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = true,
|
||||
): CurrencyIconState.CoinIcon = createCoinIcon(
|
||||
url = coin.iconUrl,
|
||||
fallbackResId = coin.networkIconResId,
|
||||
isGrayscale = isGrayscale || coin.network.isTestnet,
|
||||
showCustomBadge = coin.isCustom && showCustomBadge,
|
||||
)
|
||||
|
||||
private fun fromToken(
|
||||
token: CryptoCurrency.Token,
|
||||
isGrayscale: Boolean = false,
|
||||
showCustomBadge: Boolean = true,
|
||||
): CurrencyIconState {
|
||||
val grayScale = isGrayscale || token.network.isTestnet
|
||||
val background = token.tryGetBackgroundForTokenIcon(grayScale)
|
||||
val tint = getTintForTokenIcon(background)
|
||||
|
||||
return if (token.isCustom && token.iconUrl == null) {
|
||||
createCustomTokenIcon(
|
||||
tint = tint,
|
||||
background = background,
|
||||
topBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = grayScale,
|
||||
showCustomBadge = showCustomBadge,
|
||||
)
|
||||
} else {
|
||||
createTokenIcon(
|
||||
url = token.iconUrl,
|
||||
topBadgeIconResId = token.networkIconResId,
|
||||
isGrayscale = grayScale,
|
||||
fallbackTint = tint,
|
||||
fallbackBackground = background,
|
||||
showCustomBadge = token.isCustom && showCustomBadge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
core/ui/src/main/res/drawable/ic_copy_new_24.xml
Normal file
12
core/ui/src/main/res/drawable/ic_copy_new_24.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.099,15C21.099,13.56 21.098,12.571 20.998,11.828C20.901,11.111 20.729,10.759 20.485,10.514C20.241,10.27 19.889,10.098 19.172,10.002C18.429,9.902 17.439,9.9 16,9.9H15C13.56,9.9 12.571,9.902 11.828,10.002C11.111,10.098 10.759,10.27 10.514,10.514C10.27,10.759 10.098,11.111 10.002,11.828C9.902,12.571 9.9,13.56 9.9,15V16C9.9,17.44 9.902,18.429 10.002,19.172C10.098,19.889 10.27,20.241 10.514,20.485C10.759,20.729 11.111,20.902 11.828,20.998C12.571,21.098 13.56,21.1 15,21.1H16C17.439,21.1 18.429,21.098 19.172,20.998C19.889,20.902 20.241,20.729 20.485,20.485C20.729,20.241 20.901,19.889 20.998,19.172C21.098,18.429 21.099,17.44 21.099,16V15ZM22.9,16C22.9,17.389 22.902,18.521 22.782,19.412C22.659,20.329 22.392,21.123 21.757,21.758C21.123,22.392 20.328,22.659 19.412,22.782C18.521,22.902 17.389,22.9 16,22.9H15C13.611,22.9 12.479,22.902 11.588,22.782C10.671,22.659 9.876,22.392 9.242,21.758C8.608,21.123 8.341,20.329 8.217,19.412C8.098,18.521 8.099,17.389 8.099,16V15C8.099,13.611 8.098,12.479 8.217,11.588C8.341,10.671 8.608,9.876 9.242,9.242C9.876,8.608 10.671,8.341 11.588,8.218C12.479,8.098 13.611,8.099 15,8.099H16C17.389,8.099 18.521,8.098 19.412,8.218C20.328,8.341 21.123,8.608 21.757,9.242C22.392,9.876 22.659,10.671 22.782,11.588C22.902,12.479 22.9,13.611 22.9,15V16Z"
|
||||
android:fillColor="#919191"/>
|
||||
<path
|
||||
android:pathData="M2.099,9.999V8.999L2.099,8.945C2.099,7.581 2.099,6.466 2.217,5.587C2.34,4.67 2.607,3.876 3.241,3.241C3.876,2.607 4.67,2.34 5.587,2.217C6.466,2.099 7.581,2.099 8.945,2.099L8.999,2.099H9.999L10.054,2.099C11.417,2.099 12.532,2.099 13.411,2.217C14.328,2.34 15.123,2.607 15.757,3.241C15.978,3.463 16.16,3.709 16.307,3.983C16.542,4.42 16.378,4.966 15.941,5.201C15.503,5.437 14.957,5.273 14.722,4.835C14.656,4.713 14.579,4.608 14.484,4.514C14.24,4.27 13.888,4.098 13.171,4.001C12.428,3.901 11.439,3.9 9.999,3.9H8.999C7.559,3.9 6.57,3.901 5.827,4.001C5.11,4.098 4.758,4.27 4.514,4.514C4.269,4.758 4.097,5.11 4.001,5.827C3.901,6.57 3.899,7.559 3.899,8.999V9.999C3.899,11.439 3.901,12.428 4.001,13.171C4.097,13.888 4.269,14.24 4.514,14.484C4.692,14.663 4.923,14.797 5.292,14.895C5.772,15.022 6.059,15.515 5.932,15.995C5.812,16.446 5.371,16.726 4.921,16.654L4.831,16.635L4.606,16.569C4.09,16.404 3.633,16.149 3.241,15.757C2.607,15.123 2.34,14.328 2.217,13.411C2.099,12.532 2.099,11.417 2.099,10.054L2.099,9.999Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
38
core/ui/src/main/res/drawable/ic_ens_36.xml
Normal file
38
core/ui/src/main/res/drawable/ic_ens_36.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="36dp"
|
||||
android:height="37dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="37">
|
||||
<path
|
||||
android:pathData="M0,18.658C0,8.717 8.059,0.658 18,0.658C27.941,0.658 36,8.717 36,18.658C36,28.599 27.941,36.658 18,36.658C8.059,36.658 0,28.599 0,18.658Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="7.08"
|
||||
android:startY="6.638"
|
||||
android:endX="38.587"
|
||||
android:endY="33.361"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF8BA0FF"/>
|
||||
<item android:offset="0.524" android:color="#FF65B1FF"/>
|
||||
<item android:offset="1" android:color="#FF91D0FF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M9.314,8.766h17.372v19.784h-17.372z"/>
|
||||
<path
|
||||
android:pathData="M17.55,9.187L12.219,17.958C12.177,18.027 12.08,18.035 12.028,17.973C11.559,17.416 9.811,15.045 11.974,12.885C13.948,10.913 16.463,9.507 17.395,9.021C17.501,8.966 17.612,9.085 17.55,9.187Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M17.255,28.285C17.362,28.36 17.493,28.233 17.422,28.124C16.231,26.313 12.273,20.287 11.726,19.383C11.187,18.491 10.126,17.008 10.038,15.739C10.029,15.613 9.854,15.587 9.809,15.706C9.738,15.898 9.663,16.127 9.592,16.389C8.702,19.695 9.995,23.203 12.802,25.168L17.255,28.285Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M17.988,28.129L23.319,19.358C23.36,19.289 23.458,19.281 23.509,19.343C23.979,19.9 25.727,22.271 23.564,24.431C21.589,26.403 19.075,27.809 18.143,28.295C18.037,28.35 17.926,28.231 17.988,28.129Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M18.288,9.03C18.182,8.955 18.051,9.082 18.122,9.191C19.313,11.002 23.271,17.028 23.818,17.932C24.357,18.824 25.418,20.307 25.506,21.576C25.515,21.702 25.69,21.728 25.734,21.609C25.805,21.417 25.881,21.188 25.951,20.926C26.841,17.62 25.549,14.112 22.742,12.147L18.288,9.03Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</group>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_qrcode_new_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_qrcode_new_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4.816,11.215C3.377,11.215 2.667,10.515 2.667,9.036V4.846C2.667,3.367 3.377,2.667 4.816,2.667H9.066C10.505,2.667 11.215,3.367 11.215,4.846V9.036C11.215,10.515 10.505,11.215 9.066,11.215H4.816ZM14.934,11.215C13.485,11.215 12.785,10.515 12.785,9.036V4.846C12.785,3.367 13.485,2.667 14.934,2.667H19.184C20.624,2.667 21.333,3.367 21.333,4.846V9.036C21.333,10.515 20.624,11.215 19.184,11.215H14.934ZM4.776,9.635H9.096C9.455,9.635 9.635,9.455 9.635,9.086V4.796C9.635,4.426 9.455,4.246 9.096,4.246H4.776C4.416,4.246 4.246,4.426 4.246,4.796V9.086C4.246,9.455 4.416,9.635 4.776,9.635ZM14.894,9.635H19.224C19.584,9.635 19.754,9.455 19.754,9.086V4.796C19.754,4.426 19.584,4.246 19.224,4.246H14.894C14.535,4.246 14.355,4.426 14.355,4.796V9.086C14.355,9.455 14.535,9.635 14.894,9.635ZM6.046,8.046C5.886,8.046 5.836,7.976 5.836,7.796V6.076C5.836,5.906 5.886,5.836 6.046,5.836H7.826C7.976,5.836 8.046,5.906 8.046,6.076V7.796C8.046,7.976 7.976,8.046 7.826,8.046H6.046ZM16.204,8.046C16.054,8.046 16.004,7.976 16.004,7.796V6.076C16.004,5.906 16.054,5.836 16.204,5.836H17.984C18.144,5.836 18.204,5.906 18.204,6.076V7.796C18.204,7.976 18.144,8.046 17.984,8.046H16.204ZM4.816,21.333C3.377,21.333 2.667,20.633 2.667,19.144V14.965C2.667,13.485 3.377,12.785 4.816,12.785H9.066C10.505,12.785 11.215,13.485 11.215,14.965V19.144C11.215,20.633 10.505,21.333 9.066,21.333H4.816ZM13.475,15.454C13.325,15.454 13.275,15.384 13.275,15.204V13.475C13.275,13.315 13.325,13.245 13.475,13.245H15.254C15.414,13.245 15.474,13.315 15.474,13.475V15.204C15.474,15.384 15.414,15.454 15.254,15.454H13.475ZM18.834,15.454C18.674,15.454 18.624,15.384 18.624,15.204V13.475C18.624,13.315 18.674,13.245 18.834,13.245H20.614C20.764,13.245 20.823,13.315 20.823,13.475V15.204C20.823,15.384 20.764,15.454 20.614,15.454H18.834ZM4.776,19.754H9.096C9.455,19.754 9.635,19.574 9.635,19.204V14.915C9.635,14.535 9.455,14.365 9.096,14.365H4.776C4.416,14.365 4.246,14.535 4.246,14.915V19.204C4.246,19.574 4.416,19.754 4.776,19.754ZM16.174,18.124C16.024,18.124 15.974,18.064 15.974,17.884V16.154C15.974,15.984 16.024,15.924 16.174,15.924H17.964C18.114,15.924 18.174,15.984 18.174,16.154V17.884C18.174,18.064 18.114,18.124 17.964,18.124H16.174ZM6.046,18.154C5.886,18.154 5.836,18.094 5.836,17.914V16.184C5.836,16.014 5.886,15.954 6.046,15.954H7.826C7.976,15.954 8.046,16.014 8.046,16.184V17.914C8.046,18.094 7.976,18.154 7.826,18.154H6.046ZM13.475,20.803C13.325,20.803 13.275,20.733 13.275,20.553V18.834C13.275,18.664 13.325,18.594 13.475,18.594H15.254C15.414,18.594 15.474,18.664 15.474,18.834V20.553C15.474,20.733 15.414,20.803 15.254,20.803H13.475ZM18.834,20.803C18.674,20.803 18.624,20.733 18.624,20.553V18.834C18.624,18.664 18.674,18.594 18.834,18.594H20.614C20.764,18.594 20.823,18.664 20.823,18.834V20.553C20.823,20.733 20.764,20.803 20.614,20.803H18.834Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
|
|
@ -8,14 +8,17 @@ import com.tangem.data.common.currency.UserTokensSaver
|
|||
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
||||
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository
|
||||
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -83,4 +86,14 @@ internal object TokensDataModule {
|
|||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenReceiveWarningsViewedRepository(
|
||||
tokenReceiveWarningActionStore: TokenReceiveWarningActionStore,
|
||||
): TokenReceiveWarningsViewedRepository {
|
||||
return DefaultTokenReceiveWarningsViewedRepository(
|
||||
tokenReceiveWarningActionStore = tokenReceiveWarningActionStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
||||
internal class DefaultTokenReceiveWarningsViewedRepository(
|
||||
private val tokenReceiveWarningActionStore: TokenReceiveWarningActionStore,
|
||||
) : TokenReceiveWarningsViewedRepository {
|
||||
|
||||
override suspend fun getViewedWarnings(): Set<String> {
|
||||
return tokenReceiveWarningActionStore.getSync()
|
||||
}
|
||||
|
||||
override suspend fun view(symbol: String) {
|
||||
tokenReceiveWarningActionStore.store(symbol)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.near.NearWalletManager
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.NameResolver
|
||||
import com.tangem.blockchain.common.ResolveAddressResult
|
||||
import com.tangem.blockchain.common.ReverseResolveAddressResult
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -22,6 +23,40 @@ class DefaultWalletAddressServiceRepository(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletAddressServiceRepository {
|
||||
|
||||
override suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String? {
|
||||
return withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
walletManager?.wallet?.ens
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reverseResolveAddress(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
address: String,
|
||||
): ReverseResolveAddressResult {
|
||||
return withContext(dispatchers.io) {
|
||||
val blockchain = network.toBlockchain()
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
|
||||
if (walletManager is NameResolver) {
|
||||
walletManager.reverseResolve(address.toByteArray())
|
||||
} else {
|
||||
ReverseResolveAddressResult.NotSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resolveAddress(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.domain.models
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TokenReceiveConfig(
|
||||
val shouldShowWarning: Boolean,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val userWalletId: UserWalletId,
|
||||
val showMemoDisclaimer: Boolean,
|
||||
val receiveAddress: List<ReceiveAddressModel>,
|
||||
val tokenReceiveNotification: List<TokenReceiveNotification> = emptyList(),
|
||||
val asset: Asset = Asset.Currency,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReceiveAddressModel(
|
||||
val nameService: NameService,
|
||||
val value: String,
|
||||
val displayName: String,
|
||||
) {
|
||||
enum class NameService {
|
||||
Default, Ens
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TokenReceiveNotification(
|
||||
val title: Int,
|
||||
val subtitle: Int,
|
||||
)
|
||||
|
||||
enum class Asset {
|
||||
Currency, NFT
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.domain.models.ens
|
||||
|
||||
sealed interface EnsAddress {
|
||||
data class Address(val name: String) : EnsAddress
|
||||
data class Error(val error: Exception) : EnsAddress
|
||||
data object NotSupported : EnsAddress
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.nft
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.nft.repository.NFTRepository
|
||||
|
||||
class GetNFTCurrencyUseCase(private val nftRepository: NFTRepository) {
|
||||
|
||||
operator fun invoke(network: Network): CryptoCurrency = nftRepository.getNFTCurrency(network)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.tokens.model.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
sealed class TokenReceiveNewAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Token / Receive", event, params) {
|
||||
|
||||
class ReceiveScreenOpened(
|
||||
token: String,
|
||||
blockchainName: String,
|
||||
ensStatus: AnalyticsParam.EnsStatus,
|
||||
) : TokenReceiveNewAnalyticsEvent(
|
||||
event = "Receive Screen Opened",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchainName,
|
||||
ENS to ensStatus.value,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonCopyAddress(
|
||||
token: String,
|
||||
blockchainName: String,
|
||||
) : TokenReceiveNewAnalyticsEvent(
|
||||
event = "Button - Copy Address",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchainName,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonCopyEns(
|
||||
token: String,
|
||||
blockchainName: String,
|
||||
) : TokenReceiveNewAnalyticsEvent(
|
||||
event = "Button - ENS",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchainName,
|
||||
),
|
||||
)
|
||||
|
||||
class QrScreenOpened(
|
||||
token: String,
|
||||
blockchainName: String,
|
||||
) : TokenReceiveNewAnalyticsEvent(
|
||||
event = "QR Screen Opened",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchainName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
||||
class GetViewedTokenReceiveWarningUseCase(
|
||||
private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
|
||||
) {
|
||||
suspend operator fun invoke(): Set<String> {
|
||||
return tokenReceiveWarningsViewedRepository.getViewedWarnings()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
||||
class SaveViewedTokenReceiveWarningUseCase(
|
||||
private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository,
|
||||
) {
|
||||
suspend operator fun invoke(symbol: String) {
|
||||
tokenReceiveWarningsViewedRepository.view(symbol)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
interface TokenReceiveWarningsViewedRepository {
|
||||
|
||||
suspend fun getViewedWarnings(): Set<String>
|
||||
|
||||
suspend fun view(symbol: String)
|
||||
}
|
||||
|
|
@ -1,15 +1,24 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.blockchain.common.ResolveAddressResult
|
||||
import com.tangem.blockchain.common.ReverseResolveAddressResult
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
|
||||
/**
|
||||
* Wallet address service repository.
|
||||
*/
|
||||
interface WalletAddressServiceRepository {
|
||||
|
||||
suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String?
|
||||
|
||||
suspend fun reverseResolveAddress(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
address: String,
|
||||
): ReverseResolveAddressResult
|
||||
|
||||
suspend fun resolveAddress(userWalletId: UserWalletId, network: Network, address: String): ResolveAddressResult
|
||||
|
||||
suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
class GetEnsNameUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, address: String): String? {
|
||||
if (network.nameResolvingType != Network.NameResolvingType.ENS) {
|
||||
return null
|
||||
}
|
||||
|
||||
val addresses = walletManagersFacade.getAddresses(userWalletId, network)
|
||||
|
||||
val isOwnAddress = addresses.any { it.value == address }
|
||||
if (!isOwnAddress) {
|
||||
return null
|
||||
}
|
||||
|
||||
return walletAddressServiceRepository.getEns(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
address = address,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import com.tangem.blockchain.common.ReverseResolveAddressResult
|
||||
import com.tangem.domain.models.ens.EnsAddress
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
class GetReverseResolvedEnsAddressUseCase(private val walletAddressServiceRepository: WalletAddressServiceRepository) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
addresses: List<String>,
|
||||
): List<EnsAddress> {
|
||||
return supervisorScope {
|
||||
val addressesDeferred = addresses.map { address ->
|
||||
async {
|
||||
val result = walletAddressServiceRepository.reverseResolveAddress(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
address = address,
|
||||
)
|
||||
mapReverseResolveResult(result)
|
||||
}
|
||||
}
|
||||
addressesDeferred.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapReverseResolveResult(reverseResolveAddressResult: ReverseResolveAddressResult): EnsAddress {
|
||||
return when (reverseResolveAddressResult) {
|
||||
is ReverseResolveAddressResult.Error -> EnsAddress.Error(reverseResolveAddressResult.error)
|
||||
ReverseResolveAddressResult.NotSupported -> EnsAddress.NotSupported
|
||||
is ReverseResolveAddressResult.Resolved -> EnsAddress.Address(reverseResolveAddressResult.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -5,12 +5,20 @@ import androidx.compose.runtime.Stable
|
|||
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.arkivanov.decompose.router.slot.dismiss
|
||||
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.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
|
||||
import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -19,20 +27,41 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: MarketsPortfolioComponent.Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : AppComponentContext by context, MarketsPortfolioComponent {
|
||||
|
||||
private val model: MarketsPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) = model.setTokenNetworks(networks)
|
||||
override fun setNoNetworksAvailable() = model.setNoNetworksAvailable()
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
MyPortfolio(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
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 : MarketsPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -15,24 +17,31 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.SaveMarketTokensUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.ArtworkModel
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -43,7 +52,7 @@ import kotlinx.coroutines.sync.withLock
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsPortfolioModel @Inject constructor(
|
||||
|
|
@ -60,6 +69,9 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private val getCardImageUseCase: GetCardImageUseCase,
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
|
|
@ -80,6 +92,8 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
|
||||
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
|
|
@ -128,7 +142,11 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { updateBlock ->
|
||||
updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) }
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
|
||||
updateTokensState {
|
||||
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
|
||||
}
|
||||
}
|
||||
},
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -137,6 +155,9 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
|
||||
),
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
configureReceiveAddresses(handledAction)
|
||||
}
|
||||
},
|
||||
),
|
||||
updateTokens = { updateBlock ->
|
||||
|
|
@ -359,4 +380,51 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
block(tokensState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
|
||||
when (quickAction.action) {
|
||||
TokenActionsBSContentUM.Action.Receive -> {
|
||||
val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return
|
||||
val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency
|
||||
modelScope.launch {
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
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})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val tokenConfig = TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
|
|||
import com.tangem.features.nft.entity.NFTSendSuccessListener
|
||||
import com.tangem.features.nft.receive.NFTReceiveComponent
|
||||
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -35,6 +36,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
@Assisted private val params: NFTComponent.Params,
|
||||
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
|
||||
nftSendSuccessListener: NFTSendSuccessListener,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : NFTComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<NFTRoute>()
|
||||
|
|
@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
walletName = params.walletName,
|
||||
onBackClick = ::onChildBack,
|
||||
),
|
||||
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
|
||||
)
|
||||
|
||||
private fun getDetailsComponent(
|
||||
|
|
|
|||
|
|
@ -4,29 +4,58 @@ 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.arkivanov.decompose.router.slot.dismiss
|
||||
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.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.nft.receive.model.NFTReceiveModel
|
||||
import com.tangem.features.nft.receive.ui.NFTReceive
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class NFTReceiveComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTReceiveModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
NFTReceive(state, modifier)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val walletName: String,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.nft.receive.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -12,12 +14,20 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.Asset
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.TokenReceiveNotification
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase
|
||||
import com.tangem.domain.nft.GetNFTCurrencyUseCase
|
||||
import com.tangem.domain.nft.GetNFTNetworkStatusUseCase
|
||||
import com.tangem.domain.nft.GetNFTNetworksUseCase
|
||||
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.features.nft.receive.NFTReceiveComponent
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
|
|
@ -25,6 +35,8 @@ import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheet
|
|||
import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -43,11 +55,17 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
private val shareManager: ShareManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: NFTReceiveComponent.Params = paramsContainer.require()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
value = NFTReceiveUM(
|
||||
onBackClick = params.onBackClick,
|
||||
|
|
@ -138,14 +156,23 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
|
||||
when (val value = networkStatus.value) {
|
||||
is NetworkStatus.Verified -> {
|
||||
_state.update {
|
||||
ShowReceiveBottomSheetTransformer(
|
||||
network = network,
|
||||
networkAddress = value.address,
|
||||
onDismissBottomSheet = ::onReceiveBottomSheetDismiss,
|
||||
onCopyClick = { text -> onCopyClick(text, network) },
|
||||
onShareClick = { text -> onShareClick(text, network) },
|
||||
).transform(it)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
bottomSheetNavigation.activate(
|
||||
configuration = configureReceiveAddresses(
|
||||
addresses = value.address,
|
||||
network = network,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
_state.update {
|
||||
ShowReceiveBottomSheetTransformer(
|
||||
network = network,
|
||||
networkAddress = value.address,
|
||||
onDismissBottomSheet = ::onReceiveBottomSheetDismiss,
|
||||
onCopyClick = { text -> onCopyClick(text, network) },
|
||||
onShareClick = { text -> onShareClick(text, network) },
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkStatus.MissedDerivation,
|
||||
|
|
@ -166,4 +193,56 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name))
|
||||
shareManager.shareText(text = text)
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig {
|
||||
val cryptoCurrency = getNFTCurrencyUseCase.invoke(network)
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
network = 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.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val notifications = buildList {
|
||||
if (BlockchainUtils.isSolana(network.rawId)) {
|
||||
add(
|
||||
TokenReceiveNotification(
|
||||
title = R.string.nft_receive_unsupported_types,
|
||||
subtitle = R.string.nft_receive_unsupported_types_description,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = params.userWalletId,
|
||||
showMemoDisclaimer = false,
|
||||
receiveAddress = receiveAddresses,
|
||||
tokenReceiveNotification = notifications,
|
||||
asset = Asset.NFT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
|||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
|
||||
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
|
||||
import dagger.assisted.Assisted
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.features.onboarding.v2.twin.impl
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
|
@ -42,7 +42,6 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler { model.onBack() }
|
||||
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingTwin(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
|
@ -23,6 +24,7 @@ internal sealed class SendAnalyticEvents(
|
|||
val feeType: AnalyticsParam.FeeType,
|
||||
val blockchain: String,
|
||||
val nonceNotEmpty: Boolean,
|
||||
private val ensStatus: AnalyticsParam.EnsStatus,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Transaction Sent Screen Opened",
|
||||
params = mapOf(
|
||||
|
|
@ -30,6 +32,10 @@ internal sealed class SendAnalyticEvents(
|
|||
FEE_TYPE to feeType.value,
|
||||
BLOCKCHAIN to blockchain,
|
||||
NONCE to nonceNotEmpty.toString().capitalize(),
|
||||
ENS_ADDRESS to when (ensStatus) {
|
||||
AnalyticsParam.EnsStatus.EMPTY -> false.toString()
|
||||
AnalyticsParam.EnsStatus.FULL -> true.toString()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal class SendAnalyticHelper @Inject constructor(
|
|||
feeType = feeType,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
nonceNotEmpty = feeSelectorUM.nonce != null,
|
||||
ensStatus = getEnsStatus(sendUM),
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -52,4 +53,14 @@ internal class SendAnalyticHelper @Inject constructor(
|
|||
else -> Basic.TransactionSent.MemoType.Null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus {
|
||||
val blockchainAddressForEns =
|
||||
(sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress
|
||||
return if (blockchainAddressForEns != null) {
|
||||
AnalyticsParam.EnsStatus.FULL
|
||||
} else {
|
||||
AnalyticsParam.EnsStatus.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
1
features/token-recieve/api/.gitignore
vendored
Normal file
1
features/token-recieve/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
features/token-recieve/api/build.gradle.kts
Normal file
23
features/token-recieve/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.tokenrecieve.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
}
|
||||
|
|
@ -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<Params, TokenReceiveComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.tokenreceive
|
||||
|
||||
interface TokenReceiveFeatureToggle {
|
||||
|
||||
val isNewTokenReceiveEnabled: Boolean
|
||||
}
|
||||
1
features/token-recieve/impl/.gitignore
vendored
Normal file
1
features/token-recieve/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
58
features/token-recieve/impl/build.gradle.kts
Normal file
58
features/token-recieve/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.tokenreceive.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.configToggles)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.tokenreceive
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultTokenReceiveFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TokenReceiveFeatureToggle {
|
||||
|
||||
override val isNewTokenReceiveEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_TOKEN_RECEIVE_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.tokenreceive.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.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.decompose.ComposableContentComponent
|
||||
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.TokenReceiveContentSheet
|
||||
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<TokenReceiveRoutes>(
|
||||
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,
|
||||
tokenName = model.getTokenName(),
|
||||
),
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
internal interface TokenReceiveModelCallback :
|
||||
TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback,
|
||||
TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback,
|
||||
TokenReceiveWarningComponent.TokenReceiveWarningModelCallback
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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<NotificationUM>,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val callback: TokenReceiveAssetsModelCallback,
|
||||
val onDismiss: () -> Unit,
|
||||
val showMemoDisclaimer: Boolean,
|
||||
val fullName: String,
|
||||
val tokenName: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.tokenreceive.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class ReceiveAddress(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data object Ens : Type
|
||||
|
||||
data class Default(
|
||||
val displayName: TextReference,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.tokenreceive.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
|
||||
import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
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,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenReceiveAssetsComponent.TokenReceiveAssetsParams>()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(
|
||||
TokenReceiveNewAnalyticsEvent.ReceiveScreenOpened(
|
||||
token = params.tokenName,
|
||||
blockchainName = params.fullName,
|
||||
ensStatus = configureEnsStatus(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal val state: StateFlow<ReceiveAssetsUM>
|
||||
field = MutableStateFlow<ReceiveAssetsUM>(
|
||||
ReceiveAssetsUM(
|
||||
onCopyClick = params.callback::onCopyClick,
|
||||
onOpenQrCodeClick = params.callback::onQrCodeClick,
|
||||
addresses = params.addresses,
|
||||
showMemoDisclaimer = params.showMemoDisclaimer,
|
||||
isEnsResultLoading = false,
|
||||
notificationConfigs = params.notificationConfigs,
|
||||
fullName = params.fullName,
|
||||
),
|
||||
)
|
||||
|
||||
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
|
||||
val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens }
|
||||
return if (hasEnsAddress) {
|
||||
AnalyticsParam.EnsStatus.FULL
|
||||
} else {
|
||||
AnalyticsParam.EnsStatus.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
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.analytics.api.AnalyticsEventHandler
|
||||
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.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
|
||||
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
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@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,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), TokenReceiveModelCallback {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
val params = paramsContainer.require<TokenReceiveComponent.Params>()
|
||||
|
||||
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
|
||||
|
||||
internal val state: StateFlow<TokenReceiveUM>
|
||||
field = MutableStateFlow<TokenReceiveUM>(getInitState())
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQrCodeClick(id: Int) {
|
||||
analyticsEventHandler.send(
|
||||
TokenReceiveNewAnalyticsEvent.QrScreenOpened(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
|
||||
}
|
||||
|
||||
override fun onCopyClick(id: Int) {
|
||||
val addressToCopy = state.value.addresses[id] ?: return
|
||||
sendCopyActionAnalytic(addressToCopy)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getTokenName(): String {
|
||||
return when (val asset = params.config.asset) {
|
||||
Asset.Currency -> params.config.cryptoCurrency.symbol
|
||||
Asset.NFT -> asset.name
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapAddresses(addresses: List<ReceiveAddressModel>): ImmutableMap<Int, ReceiveAddress> {
|
||||
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<EnsAddress.Address>()
|
||||
.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<NotificationUM> {
|
||||
return buildList {
|
||||
add(
|
||||
NotificationUM.Info(
|
||||
title = resourceReference(
|
||||
R.string.receive_bottom_sheet_warning_title,
|
||||
wrappedList(
|
||||
getTokenName(),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) {
|
||||
val event = when (receiveAddress.type) {
|
||||
is ReceiveAddress.Type.Default -> {
|
||||
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
)
|
||||
}
|
||||
ReceiveAddress.Type.Ens -> {
|
||||
TokenReceiveNewAnalyticsEvent.ButtonCopyEns(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.TextReference
|
||||
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<TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams>()
|
||||
|
||||
internal val state: StateFlow<QrCodeUM>
|
||||
field = MutableStateFlow<QrCodeUM>(
|
||||
QrCodeUM(
|
||||
network = params.network,
|
||||
addressValue = params.address.value,
|
||||
addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY,
|
||||
onCopyClick = { params.callback.onCopyClick(params.id) },
|
||||
onShareClick = params.callback::onShareClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TokenReceiveWarningComponent.TokenReceiveWarningParams>()
|
||||
|
||||
internal val state: StateFlow<WarningUM>
|
||||
field = MutableStateFlow<WarningUM>(
|
||||
WarningUM(
|
||||
iconState = params.iconState,
|
||||
onWarningAcknowledged = params.callback::onWarningAcknowledged,
|
||||
network = params.network.name,
|
||||
networkIcon = params.network.iconResId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
|
||||
|
||||
@Composable
|
||||
internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
|
||||
Box {
|
||||
content()
|
||||
CopiedTextSnackbarHost(
|
||||
hostState = snackbarHostState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.buttons.small.TangemIconButton
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) {
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
|
||||
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
) {
|
||||
SpacerH8()
|
||||
|
||||
Info(
|
||||
showMemoDisclaimer = assetsUM.showMemoDisclaimer,
|
||||
notificationConfigs = assetsUM.notificationConfigs,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
AddressBlock(
|
||||
onCopyClick = assetsUM.onCopyClick,
|
||||
onOpenQrCodeClick = assetsUM.onOpenQrCodeClick,
|
||||
addresses = assetsUM.addresses,
|
||||
snackbarHostState = snackbarHostState,
|
||||
fullName = assetsUM.fullName,
|
||||
)
|
||||
|
||||
if (assetsUM.isEnsResultLoading) {
|
||||
LoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Info(
|
||||
notificationConfigs: ImmutableList<NotificationUM>,
|
||||
showMemoDisclaimer: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
|
||||
) {
|
||||
if (showMemoDisclaimer) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
notificationConfigs.fastForEach {
|
||||
key(it.hashCode()) {
|
||||
Notification(config = it.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBlock(
|
||||
onOpenQrCodeClick: (id: Int) -> Unit,
|
||||
onCopyClick: (id: Int) -> Unit,
|
||||
addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fullName: String,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
addresses.entries.toList().fastForEach { entry ->
|
||||
when (entry.value.type) {
|
||||
is ReceiveAddress.Type.Default -> {
|
||||
key(entry.key) {
|
||||
AddressItem(
|
||||
onCopyClick = {
|
||||
onCopyClick(entry.key)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(
|
||||
R.string.wallet_notification_address_copied,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onOpenQrCodeClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onOpenQrCodeClick(entry.key)
|
||||
},
|
||||
fullName = fullName,
|
||||
address = entry.value.value,
|
||||
)
|
||||
SpacerH8()
|
||||
}
|
||||
}
|
||||
ReceiveAddress.Type.Ens -> {
|
||||
key(entry.key) {
|
||||
EnsItem(
|
||||
onCopyClick = {
|
||||
onCopyClick(entry.key)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(
|
||||
R.string.wallet_notification_address_copied,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
address = entry.value.value,
|
||||
)
|
||||
SpacerH8()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressItem(
|
||||
onOpenQrCodeClick: () -> Unit,
|
||||
fullName: String,
|
||||
onCopyClick: () -> Unit,
|
||||
address: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
onClick = onOpenQrCodeClick,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 14.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IdentIcon(
|
||||
address = address,
|
||||
modifier = Modifier
|
||||
.size(size = 36.dp)
|
||||
.clip(shape = RoundedCornerShape(18.dp)),
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
EllipsisText(
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName),
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
|
||||
EllipsisText(
|
||||
text = address,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerW12()
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size28),
|
||||
iconRes = R.drawable.ic_qrcode_new_24,
|
||||
onClick = onOpenQrCodeClick,
|
||||
)
|
||||
|
||||
SpacerW8()
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size28),
|
||||
iconRes = R.drawable.ic_copy_new_24,
|
||||
onClick = onCopyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 14.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(36.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_ens_36),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
EllipsisText(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = address,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(28.dp),
|
||||
iconRes = R.drawable.ic_copy_new_24,
|
||||
onClick = onCopyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingBlock(modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveAssetsContent(
|
||||
@PreviewParameter(TokenReceiveAssetsContentProvider::class) params: ReceiveAssetsUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveAssetsContent(assetsUM = params)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<ReceiveAssetsUM> {
|
||||
val address = ReceiveAddress(
|
||||
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
|
||||
type = ReceiveAddress.Type.Default(
|
||||
displayName = stringReference("Etherium address"),
|
||||
),
|
||||
)
|
||||
private val config = ReceiveAssetsUM(
|
||||
notificationConfigs =
|
||||
persistentListOf(
|
||||
NotificationUM.Warning(
|
||||
title = stringReference("Send only XLM on the Ethereum network"),
|
||||
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
|
||||
),
|
||||
),
|
||||
addresses = persistentMapOf(
|
||||
0 to address,
|
||||
1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
|
||||
),
|
||||
showMemoDisclaimer = false,
|
||||
onCopyClick = {},
|
||||
onOpenQrCodeClick = {},
|
||||
isEnsResultLoading = true,
|
||||
fullName = "Etherium",
|
||||
)
|
||||
|
||||
override val values: Sequence<ReceiveAssetsUM>
|
||||
get() = sequenceOf(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
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.route.TokenReceiveRoutes
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveContentSheet(
|
||||
route: TokenReceiveRoutes,
|
||||
onCloseClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
contentStack: ChildStack<TokenReceiveRoutes, ComposableContentComponent>,
|
||||
) {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveContent(
|
||||
stackState: ChildStack<TokenReceiveRoutes, ComposableContentComponent>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(fade()),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.animateContentSize(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.impl.R
|
||||
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) {
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
val qrCodePainter = rememberQrPainter(content = qrCodeUM.addressValue)
|
||||
|
||||
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(bottom = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerH8()
|
||||
|
||||
QrCodePage(
|
||||
addressFullName = qrCodeUM.addressName,
|
||||
addressValue = qrCodeUM.addressValue,
|
||||
network = qrCodeUM.network,
|
||||
qrCodePainter = qrCodePainter,
|
||||
)
|
||||
SpacerH24()
|
||||
|
||||
Buttons(
|
||||
onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) },
|
||||
onCopyClick = qrCodeUM.onCopyClick,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QrCodePage(addressFullName: TextReference, addressValue: String, network: String, qrCodePainter: Painter) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) {
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.receive_bottom_sheet_warning_message_compact,
|
||||
addressFullName.resolveReference(),
|
||||
network,
|
||||
),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
Image(
|
||||
painter = qrCodePainter,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(248.dp),
|
||||
)
|
||||
|
||||
SpacerH24()
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.wc_common_address),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
SpacerH2()
|
||||
|
||||
Text(
|
||||
text = addressValue,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onShareClick: () -> Unit,
|
||||
onCopyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(id = R.string.common_copy),
|
||||
iconResId = R.drawable.ic_copy_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onCopyClick()
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(id = R.string.common_share),
|
||||
iconResId = R.drawable.ic_share_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onShareClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberQrPainter(content: String, size: Dp = 248.dp, padding: Dp = 0.dp): BitmapPainter {
|
||||
val density = LocalDensity.current
|
||||
return remember(content) {
|
||||
BitmapPainter(
|
||||
content.toQrCode(
|
||||
sizePx = with(density) { size.roundToPx() },
|
||||
paddingPx = with(density) { padding.roundToPx() },
|
||||
).asImageBitmap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveQrCodeContent(
|
||||
@PreviewParameter(TokenReceiveQrCodeContentPreviewProvider::class) qrCodeUM: QrCodeUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveQrCodeContent(qrCodeUM = qrCodeUM)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveQrCodeContentPreviewProvider : PreviewParameterProvider<QrCodeUM> {
|
||||
private val config = QrCodeUM(
|
||||
network = "Ethereum",
|
||||
addressName = stringReference("Etherium"),
|
||||
addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<QrCodeUM>
|
||||
get() = sequenceOf(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.impl.R
|
||||
import com.tangem.features.tokenreceive.ui.state.WarningUM
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveWarningContent(warningUM: WarningUM) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(size = 56.dp),
|
||||
state = warningUM.iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
iconSize = 56.dp,
|
||||
)
|
||||
|
||||
SpacerH24()
|
||||
|
||||
WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_description),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
SpacerH(48.dp)
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_got_it),
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
warningUM.onWarningAcknowledged()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = networkIcon),
|
||||
tint = Color.Unspecified,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveWarningContent(
|
||||
@PreviewParameter(TokenReceiveWarningContentProvider::class) warningUM: WarningUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveWarningContent(warningUM = warningUM)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveWarningContentProvider : PreviewParameterProvider<WarningUM> {
|
||||
val iconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = null,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
)
|
||||
|
||||
override val values: Sequence<WarningUM>
|
||||
get() = sequenceOf(
|
||||
WarningUM(
|
||||
iconState = iconState,
|
||||
onWarningAcknowledged = {},
|
||||
network = "Etherium",
|
||||
networkIcon = R.drawable.ic_eth_16,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class QrCodeUM(
|
||||
val onCopyClick: () -> Unit,
|
||||
val onShareClick: (String) -> Unit,
|
||||
val addressName: TextReference,
|
||||
val addressValue: String,
|
||||
val network: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
internal data class ReceiveAssetsUM(
|
||||
val showMemoDisclaimer: Boolean,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val onOpenQrCodeClick: (id: Int) -> Unit,
|
||||
val onCopyClick: (id: Int) -> Unit,
|
||||
val isEnsResultLoading: Boolean,
|
||||
val notificationConfigs: ImmutableList<NotificationUM>,
|
||||
val fullName: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
internal data class TokenReceiveUM(
|
||||
val network: String,
|
||||
val iconState: CurrencyIconState,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val isEnsResultLoading: Boolean,
|
||||
val notificationConfigs: ImmutableList<NotificationUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
|
||||
internal data class WarningUM(
|
||||
val network: String,
|
||||
val networkIcon: Int,
|
||||
val iconState: CurrencyIconState,
|
||||
val onWarningAcknowledged: () -> Unit,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<TokenDetailsComponent.Params>()
|
||||
|
|
@ -159,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
/** Transaction id to check for status */
|
||||
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -114,6 +115,7 @@ dependencies {
|
|||
implementation(projects.features.nft.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.kyc.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.common)
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import com.tangem.features.biometry.AskBiometryComponent
|
|||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted navigate: (WalletRoute) -> Unit,
|
||||
|
|
@ -36,6 +38,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: WalletModel = getOrCreateModel()
|
||||
|
|
@ -78,6 +81,15 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
source = AppRoute.PushNotification.Source.Main,
|
||||
),
|
||||
)
|
||||
is WalletDialogConfig.TokenReceive -> {
|
||||
tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = dialogConfig.tokenReceiveConfig,
|
||||
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import com.tangem.domain.core.utils.lceError
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
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
|
||||
|
|
@ -41,6 +43,7 @@ import com.tangem.domain.promo.models.StoryContentIds
|
|||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
|
||||
import com.tangem.domain.tokens.RemoveCurrencyUseCase
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
|
|
@ -49,6 +52,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
|||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -61,6 +65,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -128,6 +133,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
private val shareManager: ShareManager,
|
||||
private val appRouter: AppRouter,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
||||
|
||||
override fun onSendClick(
|
||||
|
|
@ -172,13 +180,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
event?.let { analyticsEventHandler.send(it) }
|
||||
|
||||
stateHolder.showBottomSheet(
|
||||
createReceiveBottomSheetContent(
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
|
||||
),
|
||||
userWalletId,
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
stateHolder.hideBottomSheet()
|
||||
modelScope.launch {
|
||||
configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let {
|
||||
router.openTokenReceiveBottomSheet(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stateHolder.showBottomSheet(
|
||||
createReceiveBottomSheetContent(
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
|
||||
),
|
||||
userWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? {
|
||||
|
|
@ -639,4 +656,45 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
targetRoute
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? {
|
||||
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
address = networkAddress.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
networkAddress.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
userWalletId = userWalletId,
|
||||
showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.router
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -98,4 +100,10 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) {
|
||||
dialogNavigation.activate(
|
||||
configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -54,4 +55,6 @@ internal interface InnerWalletRouter {
|
|||
|
||||
/** Open NFT collections screen */
|
||||
fun openNFT(userWallet: UserWallet)
|
||||
|
||||
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
|
||||
}
|
||||
|
|
@ -90,6 +90,16 @@ internal class WalletStateController @Inject constructor() {
|
|||
)
|
||||
}
|
||||
|
||||
fun hideBottomSheet() {
|
||||
update(
|
||||
transformer = OpenBottomSheetTransformer(
|
||||
userWalletId = getSelectedWalletId(),
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
onDismissBottomSheet = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialState(): WalletScreenState {
|
||||
return WalletScreenState(
|
||||
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -19,4 +20,7 @@ internal sealed interface WalletDialogConfig {
|
|||
|
||||
@Serializable
|
||||
data object AskForPushNotifications : WalletDialogConfig
|
||||
|
||||
@Serializable
|
||||
data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig
|
||||
}
|
||||
|
|
@ -278,6 +278,9 @@ include(":features:welcome:impl")
|
|||
|
||||
include(":features:account:api")
|
||||
include(":features:account:impl")
|
||||
|
||||
include(":features:token-recieve:api")
|
||||
include(":features:token-recieve:impl")
|
||||
// endregion Feature modules
|
||||
|
||||
// region Domain modules
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a
|
||||
Subproject commit 428b83bb378b615209e23afa05c88c454d06a9f1
|
||||
Loading…
Add table
Add a link
Reference in a new issue