Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-14 15:05:43 +00:00
commit 3c46ddfcf9
711 changed files with 12970 additions and 6062 deletions

1
features/biometry/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.biometry"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.biometry
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
interface AskBiometryComponent : ComposableContentComponent, ComposableBottomSheetComponent {
data class Params(
val bottomSheetVariant: Boolean,
val modelCallbacks: ModelCallbacks,
)
interface ModelCallbacks {
fun onAllowed()
fun onDenied()
}
interface Factory : ComponentFactory<Params, AskBiometryComponent>
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.biometry
interface BiometryFeatureToggles {
val isAskForBiometryEnabled: Boolean
}

1
features/biometry/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,52 @@
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.biometry.impl"
}
dependencies {
api(projects.features.biometry.api)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.res)
implementation(projects.core.decompose)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.configToggles)
implementation(projects.core.navigation)
/** Domain */
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.card)
/** Compose libraries */
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** Other */
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,76 @@
package com.tangem.features.biometry.impl
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.model.AskBiometryModel
import com.tangem.features.biometry.impl.ui.AskBiometry
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class DefaultAskBiometryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: AskBiometryComponent.Params,
) : AskBiometryComponent, AppComponentContext by appComponentContext {
private val model: AskBiometryModel = getOrCreateModel(params)
private val bsShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(true) }
init {
model.dismissBSFlow
.onEach { bsShown.value = false }
.launchIn(componentScope)
}
override fun dismiss() = model.dismiss()
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
val bsShown by bsShown.collectAsStateWithLifecycle()
val bsConfig = remember(this, bsShown) {
TangemBottomSheetConfig(
isShown = bsShown,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = bsConfig,
content = { AskBiometry(state) },
)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AskBiometry(
modifier = modifier.navigationBarsPadding(),
state = state,
)
}
@AssistedFactory
interface Factory : AskBiometryComponent.Factory {
override fun create(
context: AppComponentContext,
params: AskBiometryComponent.Params,
): DefaultAskBiometryComponent
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.biometry.impl
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.biometry.BiometryFeatureToggles
import javax.inject.Inject
internal class DefaultBiometryFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : BiometryFeatureToggles {
override val isAskForBiometryEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ASK_BIOMETRY_REFACTORING_ENABLED")
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.biometry.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.biometry.impl.DefaultAskBiometryComponent
import com.tangem.features.biometry.impl.DefaultBiometryFeatureToggles
import com.tangem.features.biometry.impl.model.AskBiometryModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface FeatureModule {
@Binds
fun bindFeatureToggles(impl: DefaultBiometryFeatureToggles): BiometryFeatureToggles
@Binds
fun bindComponentFactory(impl: DefaultAskBiometryComponent.Factory): AskBiometryComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(AskBiometryModel::class)
fun provideModel(model: AskBiometryModel): Model
}

View file

@ -0,0 +1,143 @@
package com.tangem.features.biometry.impl.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.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class AskBiometryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase,
private val settingsRepository: SettingsRepository,
private val tangemSdkManager: TangemSdkManager,
private val userWalletsListManager: UserWalletsListManager,
private val walletsRepository: WalletsRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsManager: SettingsManager,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<AskBiometryComponent.Params>()
private val _uiState = MutableStateFlow(
AskBiometryUM(
bottomSheetVariant = params.bottomSheetVariant,
onAllowClick = ::onAllowClick,
onDontAllowClick = ::dontAllow,
onDismiss = ::dismiss,
),
)
val uiState = _uiState.asStateFlow()
val dismissBSFlow = MutableSharedFlow<Unit>()
init {
modelScope.launch {
setSaveWalletScreenShownUseCase()
}
}
private fun onAllowClick() {
_uiState.update { it.copy(showProgress = true) }
allowToUseBiometrics()
}
private fun dontAllow() {
params.modelCallbacks.onDenied()
}
fun dismiss() {
modelScope.launch {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
dontAllow()
}
}
private fun allowToUseBiometrics() {
modelScope.launch {
if (tangemSdkManager.checkNeedEnrollBiometrics()) {
showEnrollBiometricsDialog()
return@launch
}
/*
* because it will be automatically saved on UserWalletsListManager switch
*/
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run {
Timber.e("Unable to save user wallet")
uiMessageSender.send(
SnackbarMessage(stringReference("No selected user wallet")),
)
return@launch
}
handleSuccessAllowing(selectedUserWallet)
}
}
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
walletsRepository.saveShouldSaveUserWallets(item = true)
settingsRepository.setShouldSaveAccessCodes(value = true)
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
if (_uiState.value.bottomSheetVariant) {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
}
params.modelCallbacks.onAllowed()
}
private fun showEnrollBiometricsDialog() {
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.save_user_wallet_agreement_enroll_biometrics_description),
title = resourceReference(R.string.save_user_wallet_agreement_enroll_biometrics_title),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_enable),
onClick = { settingsManager.openBiometricSettings() },
),
secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
),
dismissOnFirstAction = true,
),
)
}
}

View file

@ -0,0 +1,205 @@
package com.tangem.features.biometry.impl.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
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.biometry.impl.ui.state.AskBiometryUM
import com.tangem.features.biometry.R
@Composable
internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = Modifier.weight(1f),
) {
if (state.bottomSheetVariant) {
Header(onCloseClick = state.onDismiss)
}
SpacerH32()
SpacerHHalf()
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing56))
SpacerH32()
Description(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing34)
.fillMaxWidth(),
)
}
SpacerHHalf()
}
Footer(state = state)
SpacerH16()
}
}
@Composable
private fun Header(onCloseClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResourceSafe(id = R.string.common_cancel),
)
}
SpacerW8()
}
}
@Composable
private fun Title(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing32),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size56),
painter = painterResource(id = R.drawable.ic_fingerprint_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
Text(
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_header_biometrics),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun Description(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.Start,
) {
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_face_recognition_24),
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_title),
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_description),
)
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_lock_24),
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_title),
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_description_biometrics),
)
}
}
@Composable
private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = state.showProgress,
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics),
onClick = state.onAllowClick,
)
if (state.bottomSheetVariant.not()) {
SpacerH12()
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_dont_allow),
onClick = state.onDontAllowClick,
)
}
SpacerH16()
Text(
modifier = Modifier.fillMaxWidth(fraction = .7f),
text = stringResourceSafe(R.string.save_user_wallet_agreement_notice),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun DescriptionItem(iconPainter: Painter, title: String, description: String) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = iconPainter,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
SpacerW24()
Column {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerH4()
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
AskBiometry(
state = AskBiometryUM(),
)
}
}
@Preview(showBackground = true)
@Composable
private fun PreviewBS() {
TangemThemePreview {
AskBiometry(
state = AskBiometryUM(bottomSheetVariant = true),
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.biometry.impl.ui.state
import com.tangem.core.ui.extensions.TextReference
internal data class AskBiometryUM(
val bottomSheetVariant: Boolean = false,
val showProgress: Boolean = false,
val error: TextReference? = null,
val onAllowClick: () -> Unit = {},
val onDontAllowClick: () -> Unit = {},
val onDismiss: () -> Unit = {},
)

View file

@ -70,4 +70,6 @@ dependencies {
implementation(deps.kotlin.immutable.collections)
implementation(deps.reKotlin)
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.arrow.fx)
}

View file

@ -1,6 +1,6 @@
package com.tangem.features.details.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.details.model.DetailsModel
import com.tangem.features.details.model.UserWalletListModel
@ -11,7 +11,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -2,7 +2,7 @@ package com.tangem.features.details.model
import arrow.core.getOrElse
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.navigation.Router
@ -32,7 +32,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ComponentScoped
@ModelScoped
@Suppress("LongParameterList")
internal class DetailsModel @Inject constructor(
socialsBuilder: SocialsBuilder,

View file

@ -1,7 +1,7 @@
package com.tangem.features.details.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class UserWalletListModel @Inject constructor(
userWalletsFetcher: UserWalletsFetcher,
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,

View file

@ -1,7 +1,7 @@
package com.tangem.features.details.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference
@ -14,7 +14,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class ItemsBuilder @Inject constructor(private val router: Router) {
fun buildAll(

View file

@ -1,7 +1,7 @@
package com.tangem.features.details.utils
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.impl.R
@ -9,7 +9,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class SocialsBuilder @Inject constructor(
private val urlOpener: UrlOpener,
) {

View file

@ -7,7 +7,7 @@ import arrow.core.raise.fold
import arrow.core.raise.recover
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.navigation.popTo
import com.tangem.core.decompose.ui.UiMessageSender
@ -33,7 +33,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume
@ComponentScoped
@ModelScoped
@Suppress("LongParameterList")
internal class UserWalletSaver @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,

View file

@ -4,7 +4,7 @@ import arrow.core.Either
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
@ -31,7 +31,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,

View file

@ -1,6 +1,6 @@
package com.tangem.features.disclaimer.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.disclaimer.impl.model.DisclaimerModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -1,7 +1,7 @@
package com.tangem.features.disclaimer.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.navigation.Router
@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
@ModelScoped
@Suppress("LongParameterList")
internal class DisclaimerModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext

View file

@ -5,7 +5,7 @@ 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.jetpack.subscribeAsState
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

View file

@ -6,7 +6,7 @@ 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.jetpack.subscribeAsState
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

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.managetokens.model.CustomTokenFormModel
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
@ -13,7 +13,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.model
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.ui.UiMessageSender
@ -37,7 +37,7 @@ import javax.inject.Inject
// TODO: Divide to sub-components: [REDACTED_JIRA]
@Suppress("LongParameterList", "LargeClass")
@ComponentScoped
@ModelScoped
internal class CustomTokenFormModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val customCurrencyValidator: CustomCurrencyValidator,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.ui.UiMessageSender
@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class CustomTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,

View file

@ -5,7 +5,7 @@ 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.ComponentScoped
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.decompose.navigation.Router
@ -42,7 +42,7 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
@ModelScoped
internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.model
import arrow.core.flatten
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.ui.UiMessageSender
@ -37,7 +37,7 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
@ModelScoped
internal class OnboardingManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val manageTokensListManager: ManageTokensListManager,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -15,7 +15,7 @@ import com.tangem.features.managetokens.impl.R
import kotlinx.collections.immutable.persistentMapOf
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class CustomCurrencyFormBuilder @Inject constructor(
paramsContainer: ParamsContainer,
) {

View file

@ -1,7 +1,7 @@
package com.tangem.features.managetokens.utils
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
import com.tangem.domain.managetokens.CreateCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class CustomCurrencyValidator @Inject constructor(
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
private val createCustomCurrencyUseCase: CreateCurrencyUseCase,

View file

@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMap
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.resourceReference
@ -40,7 +40,7 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
@ModelScoped
internal class ManageTokensListManager @Inject constructor(
private val getManagedTokensUseCase: GetManagedTokensUseCase,
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.details.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -8,7 +8,7 @@ import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.url.UrlOpener
@ -53,7 +53,7 @@ import javax.inject.Inject
@Suppress("LargeClass", "LongParameterList")
@Stable
@ComponentScoped
@ModelScoped
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -6,7 +6,7 @@ import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack

View file

@ -11,9 +11,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.portfolio.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.ui.UiMessageSender
@ -38,7 +38,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ComponentScoped
@ModelScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,

View file

@ -2,7 +2,6 @@ package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
@ -31,7 +30,6 @@ import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.toImmutableList
@Suppress("LongParameterList")
@ComponentScoped
internal class TokenActionsHandler @AssistedInject constructor(
private val router: Router,
private val clipboardManager: ClipboardManager,

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.token.block.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -6,7 +6,7 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.navigation.Router
@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
@ModelScoped
internal class TokenMarketBlockModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -1,6 +1,6 @@
package com.tangem.features.markets.tokenlist.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.tokenlist.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@ -29,7 +29,7 @@ private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@ComponentScoped
@ModelScoped
@Stable
internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -0,0 +1,22 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.nft.api"
}
dependencies {
/* Project - Domain */
implementation(projects.domain.models)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.nft
interface NFTFeatureToggles {
val isNFTEnabled: Boolean
}

1
features/nft/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,66 @@
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.nft.impl"
}
dependencies {
/** Api */
implementation(projects.features.nft.api)
/** Core modules */
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.res)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.datasource)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.lifecycle.runtime.ktx)
/** Compose libraries */
implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
implementation(deps.lottie.compose)
implementation(deps.decompose.ext.compose)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.nft
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class DefaultNFTFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : NFTFeatureToggles {
override val isNFTEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_ENABLED")
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.nft
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NFTFeatureModule {
@Provides
@Singleton
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles {
return DefaultNFTFeatureToggles(featureTogglesManager)
}
}

View file

@ -3,4 +3,5 @@ package com.tangem.features.onboarding.v2
interface OnboardingV2FeatureToggles {
val isOnboardingV2Enabled: Boolean
val isVisaOnboardingEnabled: Boolean
val isNoteRefactoringEnabled: Boolean
}

View file

@ -15,6 +15,7 @@ dependencies {
/** Api */
implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
/** Core modules */
implementation(projects.core.configToggles)

View file

@ -10,4 +10,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED")
override val isVisaOnboardingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED")
override val isNoteRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED")
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.onboarding.v2.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.tangem.features.onboarding.v2.impl.R
@Composable
internal fun WalletCard(url: String?, modifier: Modifier = Modifier) {
AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.card_placeholder_black),
error = painterResource(R.drawable.card_placeholder_black),
fallback = painterResource(R.drawable.card_placeholder_black),
contentScale = ContentScale.Fit,
contentDescription = null,
)
}

View file

@ -4,19 +4,23 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.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.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingChildFactory
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.entry.impl.ui.OnboardingEntry
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -87,7 +91,10 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
@Suppress("MagicNumber")
private fun linkToInnerNavigation() {
// stepper linking
innerStack.observe { stack ->
innerStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
val activeChild = stack.active.instance
if (activeChild is InnerNavigationHolder) {
componentScope.launch {
@ -105,9 +112,19 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
} else {
stepperComponent.state.update {
it.copy(
currentStep = if (stack.active.configuration is OnboardingRoute.ManageTokens) 7 else 8,
steps = 8,
title = model.titleProvider.currentTitle.value,
currentStep = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> 7
is OnboardingRoute.ManageTokens -> 8
is OnboardingRoute.Done -> 9
else -> error("Unsupported route")
},
steps = 9,
title = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.ManageTokens -> resourceReference(R.string.main_manage_tokens)
is OnboardingRoute.Done -> resourceReference(R.string.onboarding_done_header)
else -> error("Unsupported route")
},
showProgress = true,
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.onboarding.v2.entry.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class OnboardingEntryEvent(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
data class Biometric(
val state: State,
) : OnboardingEntryEvent(
category = "Onboarding / Biometric",
event = "Biometry",
params = mapOf("State" to state.name),
) {
enum class State {
On, Off
}
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.features.onboarding.v2.entry.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.DefaultOnboardingEntryComponent
@ -25,7 +25,7 @@ internal interface ComponentModule {
}
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap

View file

@ -1,20 +1,25 @@
package com.tangem.features.onboarding.v2.entry.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.navigate
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.navigation.Router
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.sdk.api.TangemSdkManager
@ -25,13 +30,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
@Suppress("LongParameterList")
@ModelScoped
internal class OnboardingEntryModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val tangemSdkManager: TangemSdkManager,
private val settingsRepository: SettingsRepository,
private val askBiometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -74,34 +82,69 @@ internal class OnboardingEntryModel @Inject constructor(
titleProvider = titleProvider,
onDone = ::onVisaOnboardingDone,
)
ProductType.Note -> OnboardingRoute.Note(
scanResponse = scanResponse,
titleProvider = titleProvider,
onDone = ::navigateToFinalScreenFlow,
)
else -> error("Unsupported")
}
}
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
stackNavigation.navigate {
listOf(OnboardingRoute.ManageTokens(userWallet))
if (userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
} else {
navigateToFinalScreenFlow()
}
}
fun onManageTokensDone() {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
navigateToFinalScreenFlow()
}
private fun onVisaOnboardingDone() {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
navigateToFinalScreenFlow()
}
private fun navigateToFinalScreenFlow() {
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
modelScope.launch {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
stackNavigation.replaceAll(
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()),
)
} else {
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
} else {
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On))
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
override fun onDenied() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off))
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
private fun navigateToWalletScreen() {
modelScope.launch(NonCancellable) {
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
} else {
modelScope.launch(NonCancellable) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
}
}
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
import javax.inject.Inject
@ -12,6 +14,8 @@ internal class OnboardingChildFactory @Inject constructor(
private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory,
private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory,
private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory,
) {
fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any {
@ -47,7 +51,22 @@ internal class OnboardingChildFactory @Inject constructor(
onDone = route.onDone,
),
)
else -> Unit
is OnboardingRoute.Note -> onboardingNoteComponentFactory.create(
context = childContext,
params = OnboardingNoteComponent.Params(
scanResponse = route.scanResponse,
titleProvider = route.titleProvider,
onDone = route.onDone,
),
)
is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create(
context = childContext,
params = AskBiometryComponent.Params(
bottomSheetVariant = false,
modelCallbacks = route.modelCallbacks,
),
)
is OnboardingRoute.None -> Unit
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -10,6 +11,12 @@ sealed class OnboardingRoute : Route {
data object None : OnboardingRoute()
data class Note(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val onDone: () -> Unit,
) : OnboardingRoute()
data class MultiWallet(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
@ -28,6 +35,10 @@ sealed class OnboardingRoute : Route {
val userWallet: UserWallet,
) : OnboardingRoute()
data class AskBiometry(
val modelCallbacks: AskBiometryComponent.ModelCallbacks,
) : OnboardingRoute()
data class Done(
val onDone: () -> Unit,
) : OnboardingRoute()

View file

@ -4,14 +4,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@Composable
internal inline fun OnboardingEntry(
@ -31,29 +30,7 @@ internal inline fun OnboardingEntry(
stack = childStack,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is OnboardingRoute.MultiWallet -> {
(it.instance as OnboardingMultiWalletComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.ManageTokens -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.Done -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.Visa -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
OnboardingRoute.None -> {}
}
(it.instance as? ComposableContentComponent)?.Content(modifier = modifier)
}
}
}

View file

@ -6,11 +6,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.FaultyDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
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.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
@ -70,7 +70,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val innerNavigationStateFlow = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") {
MutableStateFlow(
MultiWalletInnerNavigationState(1, 5),
MultiWalletInnerNavigationState(1, 6),
)
}
@ -81,6 +81,8 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
backups = model.backups,
)
val backButtonClickFlow = MutableSharedFlow<Unit>()
private val childStack: Value<ChildStack<OnboardingMultiWalletState.Step, ComposableContentComponent>> =
childStack(
key = "innerStack",
@ -96,8 +98,6 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
},
)
val backButtonClickFlow = MutableSharedFlow<Unit>()
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state = innerNavigationStateFlow
@ -117,7 +117,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
source = bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = { configuration, componentContext ->
childFactory = { _, componentContext ->
MultiWalletAccessCodeComponent(
context = childByContext(componentContext),
params = childParams,

View file

@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.mode
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
@ -22,7 +22,7 @@ import javax.inject.Inject
private const val MINIMUM_ACCESS_CODE_LENGTH = 4
@Stable
@ComponentScoped
@ModelScoped
internal class MultiWalletAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -28,7 +28,7 @@ class MultiWalletBackupComponent(
params.innerNavigation.update {
it.copy(
stackSize = 5,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -7,7 +7,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.ui.UiMessageSender
@ -38,7 +38,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
@ModelScoped
@Suppress("LongParameterList")
class MultiWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,

View file

@ -27,7 +27,7 @@ class Wallet1ChooseOptionComponent(
params.innerNavigation.update {
it.copy(
stackSize = 3,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -2,7 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.mo
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.card.repository.CardRepository
@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
@ModelScoped
internal class Wallet1ChooseOptionModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,

View file

@ -0,0 +1,29 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.ui
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
internal val CarouselItems = listOf(
CarouselItem(
title = resourceReference(R.string.onboarding_wallet_info_title_first),
subtitle = resourceReference(R.string.onboarding_wallet_info_subtitle_first),
),
CarouselItem(
title = resourceReference(R.string.onboarding_wallet_info_title_second),
subtitle = resourceReference(R.string.onboarding_wallet_info_subtitle_second),
),
CarouselItem(
title = resourceReference(R.string.onboarding_wallet_info_title_third),
subtitle = resourceReference(R.string.onboarding_wallet_info_subtitle_third),
),
CarouselItem(
title = resourceReference(R.string.onboarding_wallet_info_title_fourth),
subtitle = resourceReference(R.string.onboarding_wallet_info_subtitle_fourth),
),
)
internal data class CarouselItem(
val title: TextReference,
val subtitle: TextReference,
)

View file

@ -1,10 +1,15 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.ui
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
@ -12,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -30,30 +36,44 @@ fun Wallet1ChooseOption(
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
// val pagerState = rememberPagerState(pageCount = { 10 }) // TODO [REDACTED_TASK_KEY]
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp)
.weight(1f),
.weight(1f)
.padding(top = 32.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.onboarding_wallet_info_title_first),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
val pagerState = rememberPagerState { CarouselItems.size }
Text(
text = stringResourceSafe(R.string.onboarding_wallet_info_subtitle_first),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
HorizontalPager(pagerState) { selectedIndex ->
Column(
modifier = Modifier
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val item = CarouselItems[selectedIndex]
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = item.subtitle.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
}
Dots(
modifier = Modifier.align(Alignment.CenterHorizontally),
count = CarouselItems.size,
selectedIndex = pagerState.currentPage,
)
}
@ -77,6 +97,28 @@ fun Wallet1ChooseOption(
}
}
@Composable
private fun Dots(count: Int, selectedIndex: Int, modifier: Modifier = Modifier) {
val generalColor = TangemTheme.colors.icon.informative
val selectedColor = TangemTheme.colors.icon.primary1
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
repeat(count) { index ->
val color by animateColorAsState(if (index == selectedIndex) selectedColor else generalColor)
Canvas(Modifier.size(7.dp)) {
drawCircle(
color = color,
radius = size.width / 2f,
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {

View file

@ -30,7 +30,7 @@ internal class MultiWalletCreateWalletComponent(
params.innerNavigation.update {
it.copy(
stackSize = 2,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.resourceReference
@ -34,7 +34,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ComponentScoped
@ModelScoped
internal class MultiWalletCreateWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -32,7 +32,7 @@ internal class MultiWalletFinalizeComponent(
params.innerNavigation.update {
it.copy(
stackSize = 7,
stackMaxSize = 8,
stackMaxSize = 9,
)
}
params.parentParams.titleProvider.changeTitle(

View file

@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ComponentScoped
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.card.repository.CardRepository
@ -39,7 +39,7 @@ import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ComponentScoped
@ModelScoped
internal class MultiWalletFinalizeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -61,6 +61,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private val backupCardIds = backupServiceHolder.backupService.get()?.backupCardIds.orEmpty()
private var walletHasBackupError = false
private var hasRing = false
val uiState = _uiState.asStateFlow()
val onBackFlow = MutableSharedFlow<Unit>()
@ -152,6 +153,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
val primaryCardBatchId = backupService.primaryCardBatchId ?: return
val isRing = isRing(primaryCardBatchId)
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
if (isRing) hasRing = true
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
@ -178,6 +180,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
val backupCardBatchId = backupService.backupCardsBatchIds.getOrNull(cardIndex) ?: return
val isRing = isRing(backupCardBatchId)
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
if (isRing) hasRing = true
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
@ -218,7 +221,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun finishBackup() {
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWalletCreated = createUserWallet(scanResponse)
val userWallet = when (params.parentParams.mode) {
@ -249,6 +251,10 @@ internal class MultiWalletFinalizeModel @Inject constructor(
}
}
if (hasRing) {
walletsRepository.setHasWalletsWithRing(userWallet.walletId)
}
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)

View file

@ -25,7 +25,7 @@ internal class Wallet1ScanPrimaryComponent(
params.innerNavigation.update {
it.copy(
stackSize = 4,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -2,7 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.mod
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.core.decompose.di.ComponentScoped
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.common.util.cardTypesResolver
@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
@ModelScoped
internal class Wallet1ScanPrimaryModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -39,7 +39,7 @@ internal class MultiWalletSeedPhraseComponent(
params.innerNavigation.update { st ->
st.copy(
stackSize = 3 + it.order,
stackMaxSize = 10,
stackMaxSize = 11,
)
}

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.url.UrlOpener
@ -35,7 +35,7 @@ import javax.inject.Inject
// =============================================
@Suppress("LongParameterList")
@Stable
@ComponentScoped
@ModelScoped
internal class MultiWalletSeedPhraseModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,

View file

@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -137,7 +137,6 @@ private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifie
Column(
modifier = modifier.fillMaxWidth(),
) {
// TODO migrate to material3
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()

View file

@ -84,8 +84,8 @@ private fun SegmentSeedBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase,
Text(
text = pluralStringResourceSafe(
id = R.plurals.onboarding_seed_generate_words_count,
count = state.option.length,
state.option.length,
count = it.length,
it.length,
),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing10)

View file

@ -1,6 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMultiWalletComponent
@ -30,7 +30,7 @@ internal interface ComponentModule {
}
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -1,13 +1,14 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
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.decompose.navigation.Router
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
@ModelScoped
internal class OnboardingMultiWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -116,8 +117,13 @@ internal class OnboardingMultiWalletModel @Inject constructor(
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup &&
scanResponse.primaryCard == null -> OnboardingMultiWalletState.Step.ScanPrimary
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup ->
OnboardingMultiWalletState.Step.AddBackupDevice
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
if (scanResponse.productType == ProductType.Wallet) {
OnboardingMultiWalletState.Step.ChooseBackupOption
} else {
OnboardingMultiWalletState.Step.AddBackupDevice
}
}
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true ->
OnboardingMultiWalletState.Step.Finalize
else ->

View file

@ -5,13 +5,13 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = when (step) {
OnboardingMultiWalletState.Step.CreateWallet ->
resourceReference(R.string.onboarding_create_wallet_header)
OnboardingMultiWalletState.Step.SeedPhrase,
OnboardingMultiWalletState.Step.CreateWallet,
-> resourceReference(R.string.onboarding_create_wallet_header)
OnboardingMultiWalletState.Step.ScanPrimary,
OnboardingMultiWalletState.Step.AddBackupDevice,
->
resourceReference(R.string.onboarding_navbar_title_creating_backup)
OnboardingMultiWalletState.Step.Finalize ->
resourceReference(R.string.onboarding_button_finalize_backup)
else -> error("No title for the step")
-> resourceReference(R.string.onboarding_navbar_title_creating_backup)
OnboardingMultiWalletState.Step.ChooseBackupOption -> resourceReference(R.string.onboarding_getting_started)
OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup)
OnboardingMultiWalletState.Step.Done -> resourceReference(R.string.common_done)
}

View file

@ -51,13 +51,13 @@ internal fun OnboardingMultiWallet(
modifier = Modifier
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
.weight(.4f)
.weight(.48f)
.fillMaxWidth(),
state = artworksState,
)
Box(
modifier = Modifier.weight(.44f),
modifier = Modifier.weight(.52f),
contentAlignment = Alignment.BottomStart,
) {
childContent(Modifier)

View file

@ -2,10 +2,12 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@ -13,18 +15,14 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.common.ui.WalletCard
import kotlinx.coroutines.delay
import java.util.concurrent.TimeUnit
@ -106,7 +104,7 @@ fun WalletArtworks(
}
}
val circleColor = TangemTheme.colors.background.secondary
val circleColor = TangemTheme.colors.button.secondary
Canvas(
modifier = Modifier
@ -241,36 +239,21 @@ private fun AnimatedWalletCards(
}
}
@Composable
private fun WalletCard(url: String?, modifier: Modifier = Modifier) {
AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.card_placeholder_black),
error = painterResource(R.drawable.card_placeholder_black),
fallback = painterResource(R.drawable.card_placeholder_black),
contentDescription = null,
)
}
private fun WalletArtworksState.toTransitionSetState(maxWidthDp: Float, maxHeightDp: Float, density: Float) =
when (this) {
WalletArtworksState.Hidden -> listOf()
is WalletArtworksState.Folded -> listOf(
CardsTransitionState(
WalletCardTransitionState(),
WalletCardTransitionState(),
WalletCardTransitionState(),
WalletCardTransitionState(alpha = 1f),
WalletCardTransitionState(alpha = 0f),
WalletCardTransitionState(alpha = 0f),
),
)
is WalletArtworksState.Stack -> listOf(
CardsTransitionState(
walletCard1 = WalletCardTransitionState(),
walletCard2 = WalletCardTransitionState(),
walletCard3 = WalletCardTransitionState(),
WalletCardTransitionState(alpha = 1f),
WalletCardTransitionState(alpha = 0f),
WalletCardTransitionState(alpha = 0f),
),
CardsTransitionState(
walletCard1 = WalletCardTransitionState().copy(
@ -451,11 +434,14 @@ private fun WalletArtworksState.toTransitionSetState(maxWidthDp: Float, maxHeigh
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
var state: WalletArtworksState by remember { mutableStateOf(WalletArtworksState.Folded) }

View file

@ -0,0 +1,18 @@
package com.tangem.features.onboarding.v2.note.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingNoteComponent : ComposableContentComponent, InnerNavigationHolder {
data class Params(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val onDone: () -> Unit,
)
interface Factory : ComponentFactory<Params, OnboardingNoteComponent>
}

View file

@ -0,0 +1,148 @@
package com.tangem.features.onboarding.v2.note.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.subscribe
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
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.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
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.child.topup.OnboardingNoteTopUpComponent
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
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted val params: OnboardingNoteComponent.Params,
) : OnboardingNoteComponent, AppComponentContext by context {
private val model: OnboardingNoteModel = getOrCreateModel(params)
private val currentChildBackEventHandle = instanceKeeper.getOrCreateSimple(key = "currentChildBackEventHandle") {
MutableSharedFlow<Unit>()
}
private val childParams = ChildParams(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
)
private val childStack: Value<ChildStack<OnboardingNoteRoute, ComposableContentComponent>> =
childStack(
key = "innerStack",
source = model.stackNavigation,
serializer = null,
initialConfiguration = model.initialRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createChild(
configuration,
childByContext(factoryContext),
)
},
)
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state: StateFlow<InnerNavigationState> = model.innerNavigationState
override fun pop(onComplete: (Boolean) -> Unit) {
// TODO [REDACTED_TASK_KEY]
}
}
init {
componentScope.launch {
model.onDone.collect { params.onDone() }
}
// sets title and stepper value
childStack.subscribe(lifecycle) { stack ->
val currentRoute = stack.active.configuration
params.titleProvider.changeTitle(TextReference.Res(R.string.onboarding_title))
model.updateStepForNewRoute(currentRoute)
}
}
private fun createChild(
route: OnboardingNoteRoute,
factoryContext: AppComponentContext,
): ComposableContentComponent {
return when (route) {
OnboardingNoteRoute.CreateWallet -> OnboardingNoteCreateWalletComponent(
appComponentContext = factoryContext,
params = OnboardingNoteCreateWalletComponent.Params(
childParams = childParams,
onDone = { model.stackNavigation.push(OnboardingNoteRoute.TopUp) },
),
)
OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent(
appComponentContext = factoryContext,
params = OnboardingNoteTopUpComponent.Params(
childParams = childParams,
onDone = { model.onDone.tryEmit(Unit) },
),
)
}
}
private fun onChildBack() = model.onChildBack(
currentRoute = childStack.value.active.configuration,
)
@Composable
override fun Content(modifier: Modifier) {
val stackState by childStack.subscribeAsState()
Children(
stack = stackState,
animation = stackAnimation(slide()),
) {
it.instance.Content(modifier)
}
}
@AssistedFactory
interface Factory : OnboardingNoteComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnboardingNoteComponent.Params,
): DefaultOnboardingNoteComponent
}
data class ChildParams(
val onBack: () -> Unit,
val parentBackEvent: SharedFlow<Unit>,
)
}
internal data class OnboardingNoteInnerNavigationState(
override val stackSize: Int,
) : InnerNavigationState {
override val stackMaxSize: Int = ONBOARDING_NOTE_STEPS_COUNT
}
internal const val ALL_STEPS_TOP_CONTAINER_WEIGHT = 0.48f

View file

@ -0,0 +1,39 @@
package com.tangem.features.onboarding.v2.note.impl.child.create
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel
import com.tangem.features.onboarding.v2.note.impl.child.create.ui.OnboardingNoteCreateWallet
internal class OnboardingNoteCreateWalletComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingNoteCreateWalletModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
OnboardingNoteCreateWallet(
modifier = modifier,
state = state,
)
}
data class Params(
val childParams: DefaultOnboardingNoteComponent.ChildParams,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.onboarding.v2.note.impl.child.create.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.note.impl.child.create.ui.state.OnboardingNoteCreateWalletUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Suppress("UnusedPrivateMember")
@ModelScoped
internal class OnboardingNoteCreateWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
) : Model() {
private val _uiState = MutableStateFlow(
OnboardingNoteCreateWalletUM(),
)
val uiState: StateFlow<OnboardingNoteCreateWalletUM> = _uiState
}

View file

@ -0,0 +1,92 @@
package com.tangem.features.onboarding.v2.note.impl.child.create.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd
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.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworks
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState
import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT
import com.tangem.features.onboarding.v2.note.impl.child.create.ui.state.OnboardingNoteCreateWalletUM
@Composable
internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
WalletArtworks(
url1 = state.artworkUrl,
url2 = null,
url3 = null,
modifier = Modifier
.padding(horizontal = 34.dp)
.padding(top = 56.dp)
.weight(ALL_STEPS_TOP_CONTAINER_WEIGHT)
.fillMaxWidth(),
state = WalletArtworksState.Folded,
)
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(32.dp)
.weight(1.0f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.onboarding_create_wallet_header),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = stringResource(R.string.onboarding_create_wallet_body),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.onboarding_create_wallet_button_create_wallet),
onClick = state.onCreateClick,
showProgress = state.createWalletInProgress,
)
}
}
@Preview(showBackground = true)
@Composable
private fun OnboardingNoteCreatePreview() {
TangemThemePreview {
OnboardingNoteCreateWallet(
state = OnboardingNoteCreateWalletUM(
artworkUrl = null,
createWalletInProgress = false,
onCreateClick = {},
),
)
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.onboarding.v2.note.impl.child.create.ui.state
data class OnboardingNoteCreateWalletUM(
val artworkUrl: String? = null,
val createWalletInProgress: Boolean = false,
val onCreateClick: () -> Unit = {},
)

View file

@ -0,0 +1,39 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp
internal class OnboardingNoteTopUpComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingNoteTopUpModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
OnboardingNoteTopUp(
modifier = modifier,
state = state,
)
}
data class Params(
val childParams: DefaultOnboardingNoteComponent.ChildParams,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Suppress("UnusedPrivateMember")
@ModelScoped
internal class OnboardingNoteTopUpModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(
OnboardingNoteTopUpUM(),
)
val uiState: StateFlow<OnboardingNoteTopUpUM> = _uiState
}

View file

@ -0,0 +1,125 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
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.onboarding.v2.common.ui.WalletCard
import com.tangem.features.onboarding.v2.impl.R
@Composable
fun OnboardinNoteTopUpHeader(
balance: String?,
cardArtworkUrl: String?,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.padding(vertical = 24.dp, horizontal = 32.dp)
.fillMaxSize()
.background(
TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 32.dp),
) {
WalletCard(
modifier = Modifier.width(120.dp),
url = cardArtworkUrl,
)
SpacerHMax()
Text(
text = stringResourceSafe(R.string.common_balance_title),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = balance.orEmpty(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerHMax()
RefreshButton(
isRefreshing = isRefreshing,
onRefreshBalanceClick = onRefreshBalanceClick,
)
}
}
}
@Composable
private fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size48)
.shadow(elevation = 2.dp, shape = CircleShape)
.background(TangemTheme.colors.background.action)
.clickable(
enabled = !isRefreshing,
onClick = onRefreshBalanceClick,
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
),
) {
if (isRefreshing) {
Box {
CircularProgressIndicator(
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_refresh_24),
contentDescription = null,
tint = TangemTheme.colors.text.disabled,
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun OnboardinNoteTopUpHeaderPreview() {
TangemThemePreview {
OnboardinNoteTopUpHeader(
balance = "0.00000001 BTC",
cardArtworkUrl = "",
onRefreshBalanceClick = {},
isRefreshing = false,
)
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH8
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.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
@Composable
fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
OnboardinNoteTopUpHeader(
balance = state.balance,
cardArtworkUrl = state.cardArtworkUrl,
onRefreshBalanceClick = state.onRefreshBalanceClick,
isRefreshing = state.isRefreshing,
modifier = Modifier
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
.weight(ALL_STEPS_TOP_CONTAINER_WEIGHT)
.fillMaxWidth(),
)
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(32.dp)
.weight(1.0f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.onboarding_topup_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = stringResourceSafe(R.string.onboarding_top_up_body),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
BottomButtons(state)
}
}
@Composable
private fun BottomButtons(state: OnboardingNoteTopUpUM) {
if (state.availableForBuy) {
PrimaryButtonIconEnd(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto),
onClick = state.onBuyCryptoClick,
)
SpacerH8()
SecondaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address),
onClick = state.onShowWalletAddressClick,
)
} else {
PrimaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.onboarding_button_receive_crypto),
onClick = state.onShowWalletAddressClick,
)
}
}
@Preview(showBackground = true)
@Composable
private fun OnboardingNoteTopUpPreview() {
TangemThemePreview {
OnboardingNoteTopUp(
state = OnboardingNoteTopUpUM(
availableForBuy = true,
),
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
data class OnboardingNoteTopUpUM(
val cardArtworkUrl: String? = null,
val availableForBuy: Boolean = true,
val balance: String? = null,
val isRefreshing: Boolean = false,
val amountToCreateAccount: String? = null,
val onBuyCryptoClick: () -> Unit = {},
val onShowWalletAddressClick: () -> Unit = {},
val onRefreshBalanceClick: () -> Unit = {},
)

View file

@ -0,0 +1,45 @@
package com.tangem.features.onboarding.v2.note.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import dagger.Binds
import dagger.Module
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 interface ComponentModule {
@Binds
@Singleton
fun bindComponent(factory: DefaultOnboardingNoteComponent.Factory): OnboardingNoteComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(OnboardingNoteModel::class)
fun provideNoteModel(model: OnboardingNoteModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingNoteCreateWalletModel::class)
fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingNoteTopUpModel::class)
fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model
}

View file

@ -0,0 +1,58 @@
package com.tangem.features.onboarding.v2.note.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
import com.tangem.features.onboarding.v2.note.impl.route.stepNum
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@ModelScoped
internal class OnboardingNoteModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<OnboardingNoteComponent.Params>()
private val _currentScanResponse = MutableStateFlow(params.scanResponse)
val initialRoute = OnboardingNoteRoute.CreateWallet // TODO [REDACTED_TASK_KEY]
private val _innerNavigationState =
MutableStateFlow(OnboardingNoteInnerNavigationState(stackSize = initialRoute.stepNum()))
val currentScanResponse = _currentScanResponse.asStateFlow()
val innerNavigationState = _innerNavigationState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
val stackNavigation = StackNavigation<OnboardingNoteRoute>()
private val _uiState = MutableStateFlow(
OnboardingNoteState(),
)
val uiState: StateFlow<OnboardingNoteState> = _uiState
fun updateStepForNewRoute(route: OnboardingNoteRoute) {
_innerNavigationState.value = innerNavigationState.value.copy(
stackSize = route.stepNum(),
)
}
@Suppress("UnusedPrivateMember")
fun onChildBack(currentRoute: OnboardingNoteRoute) {
// TODO [REDACTED_TASK_KEY]
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.onboarding.v2.note.impl.model
data class OnboardingNoteState(
val cardArtworkUrl: String? = null,
)

View file

@ -0,0 +1,15 @@
package com.tangem.features.onboarding.v2.note.impl.route
import kotlinx.serialization.Serializable
@Serializable
internal sealed class OnboardingNoteRoute {
@Serializable
data object CreateWallet : OnboardingNoteRoute()
@Serializable
data object TopUp : OnboardingNoteRoute()
}
internal const val ONBOARDING_NOTE_STEPS_COUNT = 3

Some files were not shown because too many files have changed in this diff Show more