Updated on 2026-08-14
This commit is contained in:
commit
6920ad3b6d
356 changed files with 7941 additions and 1194 deletions
|
|
@ -23,7 +23,6 @@ import com.tangem.domain.redux.ReduxStateHolder
|
|||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.SaveWalletError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
|
||||
import com.tangem.features.details.impl.R
|
||||
|
|
@ -38,7 +37,7 @@ import kotlin.coroutines.resume
|
|||
internal class UserWalletSaver @Inject constructor(
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val messageSender: UiMessageSender,
|
||||
|
|
@ -56,17 +55,20 @@ internal class UserWalletSaver @Inject constructor(
|
|||
}
|
||||
},
|
||||
ifRight = { scanResponse ->
|
||||
recover(block = {
|
||||
scanResponse ?: return
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWallet(userWallet)
|
||||
}, recover = {
|
||||
recover(
|
||||
block = {
|
||||
scanResponse ?: return
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWallet(userWallet)
|
||||
},
|
||||
recover = {
|
||||
val message = it.message
|
||||
|
||||
if (!message.isNullOrEmpty()) {
|
||||
messageSender.send(SnackbarMessage(message))
|
||||
}
|
||||
},)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -110,7 +112,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
|
||||
val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build()
|
||||
val userWallet = userWalletBuilderFactory.create(scanResponse = response).build()
|
||||
|
||||
return ensureNotNull(userWallet) { Error.Unknown }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ internal class MarketsTokenItemConverter(
|
|||
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
val scaled = percent.setScale(4, RoundingMode.HALF_UP)
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ dependencies {
|
|||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.nft.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ interface OnboardingV2FeatureToggles {
|
|||
val isOnboardingV2Enabled: Boolean
|
||||
val isVisaOnboardingEnabled: Boolean
|
||||
val isNoteRefactoringEnabled: Boolean
|
||||
val isTwinRefactoringEnabled: Boolean
|
||||
}
|
||||
|
|
@ -43,6 +43,9 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.onboarding)
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.onramp)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
|
@ -63,6 +66,7 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.lottie.compose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
|
|||
get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED")
|
||||
override val isNoteRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED")
|
||||
override val isTwinRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.analytics
|
||||
package com.tangem.features.onboarding.v2.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
sealed class OnboardingEvent(
|
||||
category: String,
|
||||
|
|
@ -38,6 +40,21 @@ sealed class OnboardingEvent(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class Topup(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingEvent("Onboarding / Top Up", event, params) {
|
||||
|
||||
object ScreenOpened : Topup("Activation Screen Opened")
|
||||
|
||||
object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")
|
||||
|
||||
class ButtonBuyCrypto(currency: CryptoCurrency) : Topup(
|
||||
event = "Button - Buy Crypto",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.symbol),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Backup(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(R.string.onboarding_exit_alert_title),
|
||||
message = resourceReference(R.string.onboarding_exit_alert_message),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_yes),
|
||||
onClick = onConfirm,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
|
||||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
@Composable
|
||||
internal 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) {
|
||||
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.primary1,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,14 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
|
||||
interface OnboardingDoneComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val onDone: () -> Unit)
|
||||
data class Params(
|
||||
val mode: Mode,
|
||||
val onDone: () -> Unit,
|
||||
)
|
||||
|
||||
enum class Mode {
|
||||
WalletCreated, GoodToGo
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingDoneComponent>
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.done.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
|
|
@ -18,7 +17,8 @@ internal class DefaultOnboardingDoneComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
OnboardingDone(
|
||||
onContinueClick = remember(this) { params.onDone },
|
||||
mode = params.mode,
|
||||
onContinueClick = params.onDone,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,16 @@ 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.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
internal fun OnboardingDone(
|
||||
mode: OnboardingDoneComponent.Mode,
|
||||
onContinueClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
|
||||
val progress by animateLottieCompositionAsState(composition)
|
||||
var showConfetti by remember { mutableStateOf(false) }
|
||||
|
|
@ -57,7 +63,12 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo
|
|||
SpacerH12()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
text = stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding),
|
||||
text = when (mode) {
|
||||
OnboardingDoneComponent.Mode.WalletCreated ->
|
||||
stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
|
||||
OnboardingDoneComponent.Mode.GoodToGo ->
|
||||
stringResourceSafe(R.string.visa_onboarding_success_screen_description)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -100,6 +111,9 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo
|
|||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
OnboardingDone(onContinueClick = {})
|
||||
OnboardingDone(
|
||||
mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
onContinueClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ 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.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
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
|
||||
|
|
@ -105,7 +106,12 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
|
||||
when {
|
||||
params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
onDone = ::navigateToWalletScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed() -> {
|
||||
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
|
||||
|
|
@ -117,34 +123,58 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onVisaOnboardingDone() {
|
||||
navigateToFinalScreenFlow()
|
||||
navigateToFinalScreenFlow(doneMode = OnboardingDoneComponent.Mode.GoodToGo)
|
||||
}
|
||||
|
||||
private fun navigateToFinalScreenFlow() {
|
||||
private fun navigateToFinalScreenFlow(
|
||||
doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
) {
|
||||
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
|
||||
modelScope.launch {
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()),
|
||||
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)),
|
||||
)
|
||||
} else {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::navigateToWalletScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::navigateToWalletScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
|
||||
inner class AskBiometryModelCallbacks(
|
||||
private val doneMode: OnboardingDoneComponent.Mode,
|
||||
) : AskBiometryComponent.ModelCallbacks {
|
||||
override fun onAllowed() {
|
||||
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On))
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::navigateToWalletScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off))
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::navigateToWalletScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ internal class OnboardingChildFactory @Inject constructor(
|
|||
is OnboardingRoute.Done -> onboardingDoneComponentFactory.create(
|
||||
context = childContext,
|
||||
params = OnboardingDoneComponent.Params(
|
||||
mode = route.mode,
|
||||
onDone = route.onDone,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
|
||||
sealed class OnboardingRoute : Route {
|
||||
|
|
@ -40,6 +41,7 @@ sealed class OnboardingRoute : Route {
|
|||
) : OnboardingRoute()
|
||||
|
||||
data class Done(
|
||||
val mode: OnboardingDoneComponent.Mode,
|
||||
val onDone: () -> Unit,
|
||||
) : OnboardingRoute()
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ 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.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -19,8 +20,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog
|
||||
|
|
@ -48,6 +49,7 @@ class MultiWalletBackupModel @Inject constructor(
|
|||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
|
|
@ -241,7 +243,7 @@ class MultiWalletBackupModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
else -> FirebaseCrashlytics.getInstance().recordException(result.error)
|
||||
else -> analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -25,7 +24,7 @@ import javax.inject.Inject
|
|||
internal class Wallet1ChooseOptionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
|
|
@ -67,7 +66,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(),
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
|
||||
|
|
@ -43,7 +42,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -146,7 +145,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(),
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class MultiWalletCreateWalletUM(
|
||||
val title: TextReference,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
|
|
@ -49,7 +48,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -279,7 +278,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase)
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse)
|
||||
.backupCardsIds(backupCardIds.toSet())
|
||||
.hasBackupError(walletHasBackupError)
|
||||
.build(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state
|
||||
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class MultiWalletFinalizeUM(
|
||||
val step: Step = Step.Primary,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import com.tangem.domain.card.repository.CardRepository
|
|||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) =
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.domain.models.scan.ProductType
|
|||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
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
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
|
||||
|
|
@ -18,21 +18,25 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMul
|
|||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class OnboardingMultiWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
analyticsHandler: AnalyticsEventHandler,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val backupServiceHolder: BackupServiceHolder,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val getCardImageUseCase: GetCardImageUseCase,
|
||||
) : Model() {
|
||||
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
|
||||
private val getCardImageUseCase = GetCardImageUseCase()
|
||||
private val _uiState = MutableStateFlow(OnboardingMultiWalletUM())
|
||||
|
||||
val state = MutableStateFlow(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state
|
||||
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class OnboardingMultiWalletUM(
|
||||
val artwork1Url: String? = null,
|
||||
|
|
|
|||
|
|
@ -25,15 +25,16 @@ 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.model.OnboardingNoteCommonState
|
||||
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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
|
|
@ -47,6 +48,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private val childParams = ChildParams(
|
||||
commonState = model.commonUiState,
|
||||
onBack = ::onChildBack,
|
||||
parentBackEvent = currentChildBackEventHandle,
|
||||
)
|
||||
|
|
@ -70,15 +72,13 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
|||
override val state: StateFlow<InnerNavigationState> = model.innerNavigationState
|
||||
|
||||
override fun pop(onComplete: (Boolean) -> Unit) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
model.onChildBack(
|
||||
currentRoute = childStack.value.active.configuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
componentScope.launch {
|
||||
model.onDone.collect { params.onDone() }
|
||||
}
|
||||
|
||||
// sets title and stepper value
|
||||
childStack.subscribe(lifecycle) { stack ->
|
||||
val currentRoute = stack.active.configuration
|
||||
|
|
@ -96,14 +96,17 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
|||
appComponentContext = factoryContext,
|
||||
params = OnboardingNoteCreateWalletComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = { model.stackNavigation.push(OnboardingNoteRoute.TopUp) },
|
||||
onWalletCreated = { userWallet ->
|
||||
model.onWalletCreated(userWallet)
|
||||
model.stackNavigation.push(OnboardingNoteRoute.TopUp)
|
||||
},
|
||||
),
|
||||
)
|
||||
OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = OnboardingNoteTopUpComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = { model.onDone.tryEmit(Unit) },
|
||||
onDone = { params.onDone() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -134,6 +137,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
data class ChildParams(
|
||||
val commonState: MutableStateFlow<OnboardingNoteCommonState>,
|
||||
val onBack: () -> Unit,
|
||||
val parentBackEvent: SharedFlow<Unit>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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.domain.wallets.models.UserWallet
|
||||
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
|
||||
|
|
@ -24,7 +25,7 @@ internal class OnboardingNoteCreateWalletComponent(
|
|||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
|
||||
BackHandler(onBack = remember(this) { { params.childParams.onBack } })
|
||||
|
||||
OnboardingNoteCreateWallet(
|
||||
modifier = modifier,
|
||||
|
|
@ -34,6 +35,6 @@ internal class OnboardingNoteCreateWalletComponent(
|
|||
|
||||
data class Params(
|
||||
val childParams: DefaultOnboardingNoteComponent.ChildParams,
|
||||
val onDone: () -> Unit,
|
||||
val onWalletCreated: (UserWallet) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,26 +1,103 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.create.model
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.core.analytics.Analytics
|
||||
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
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
|
||||
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 kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@ModelScoped
|
||||
internal class OnboardingNoteCreateWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteCreateWalletComponent.Params>()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
OnboardingNoteCreateWalletUM(),
|
||||
OnboardingNoteCreateWalletUM(
|
||||
onCreateClick = ::onCreateWalletClick,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened)
|
||||
modelScope.launch {
|
||||
val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch
|
||||
if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) {
|
||||
Analytics.send(OnboardingEvent.Started)
|
||||
}
|
||||
}
|
||||
observeArtwork()
|
||||
}
|
||||
|
||||
val uiState: StateFlow<OnboardingNoteCreateWalletUM> = _uiState
|
||||
|
||||
private fun onCreateWalletClick() {
|
||||
modelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(createWalletInProgress = true)
|
||||
}
|
||||
val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch
|
||||
val result = tangemSdkManager.createProductWallet(scanResponse)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully())
|
||||
cardRepository.startCardActivation(scanResponse.card.cardId)
|
||||
createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card))
|
||||
}
|
||||
is CompletionResult.Failure -> _uiState.update {
|
||||
it.copy(createWalletInProgress = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWalletAndNavigateBackWithDone(scanResponse: ScanResponse) {
|
||||
modelScope.launch {
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWalletUseCase(userWallet, canOverride = true).onRight {
|
||||
params.onWalletCreated(userWallet)
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(createWalletInProgress = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeArtwork() {
|
||||
modelScope.launch {
|
||||
params.childParams.commonState.collect {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
artworkUrl = it.cardArtworkUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,253 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.model
|
||||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent
|
||||
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 com.tangem.utils.extensions.isPositive
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class OnboardingNoteTopUpModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteTopUpComponent.Params>()
|
||||
private val commonState = params.childParams.commonState
|
||||
private val scanResponse = params.childParams.commonState.value.scanResponse
|
||||
private var userWallet = params.childParams.commonState.value.userWallet
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
OnboardingNoteTopUpUM(),
|
||||
OnboardingNoteTopUpUM(
|
||||
onRefreshBalanceClick = ::refreshBalance,
|
||||
onBuyCryptoClick = ::onBuyCryptoClick,
|
||||
onShowWalletAddressClick = ::onShowWalletAddressClick,
|
||||
onDismissBottomSheet = ::onDismissBottomSheet,
|
||||
),
|
||||
)
|
||||
|
||||
val uiState: StateFlow<OnboardingNoteTopUpUM> = _uiState
|
||||
|
||||
init {
|
||||
Analytics.send(OnboardingEvent.Topup.ScreenOpened)
|
||||
observeArtwork()
|
||||
modelScope.launch {
|
||||
createUserWalletIfNull()
|
||||
observeCryptoCurrencyStatus()
|
||||
refreshBalance()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshBalance() {
|
||||
modelScope.launch {
|
||||
showBalanceLoadingProgress(true)
|
||||
createUserWalletIfNull()
|
||||
val userWalletId = requireNotNull(userWallet?.walletId)
|
||||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
refresh = true,
|
||||
)
|
||||
showBalanceLoadingProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBuyCryptoClick() {
|
||||
val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
|
||||
modelScope.launch {
|
||||
getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight {
|
||||
urlOpener.openUrl(it)
|
||||
}
|
||||
}
|
||||
Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency))
|
||||
}
|
||||
|
||||
private fun onShowWalletAddressClick() {
|
||||
val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
|
||||
val networkAddress = currencyStatus.value.networkAddress ?: return
|
||||
|
||||
_uiState.update {
|
||||
it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress))
|
||||
}
|
||||
Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress)
|
||||
}
|
||||
|
||||
private fun onDismissBottomSheet() {
|
||||
_uiState.update {
|
||||
it.copy(addressBottomSheetConfig = null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createUserWalletIfNull() {
|
||||
if (userWallet != null) {
|
||||
return
|
||||
}
|
||||
val commonState = params.childParams.commonState.value
|
||||
userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse)
|
||||
}
|
||||
|
||||
private fun observeArtwork() {
|
||||
modelScope.launch {
|
||||
params.childParams.commonState.collect {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
cardArtworkUrl = it.cardArtworkUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeCryptoCurrencyStatus() {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId).map { it.getOrNull() }.filterNotNull()
|
||||
.onEach(::applyCryptoCurrencyStatusToState).launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
|
||||
if (commonState.value.cryptoCurrencyStatus == null) {
|
||||
loadAvailableForBuy(status)
|
||||
}
|
||||
|
||||
commonState.update {
|
||||
it.copy(cryptoCurrencyStatus = status)
|
||||
}
|
||||
|
||||
val amount = when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded -> status.value.amount
|
||||
is CryptoCurrencyStatus.NoAccount -> status.value.amount
|
||||
is CryptoCurrencyStatus.NoQuote -> status.value.amount
|
||||
else -> null
|
||||
}
|
||||
val hasCurrentNetworkTransactions = when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions
|
||||
is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions
|
||||
else -> false
|
||||
}
|
||||
val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount
|
||||
|
||||
if (amount?.isPositive() == true || hasCurrentNetworkTransactions) {
|
||||
modelScope.launch {
|
||||
cardRepository.finishCardActivation(scanResponse.card.cardId)
|
||||
params.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
amountToCreateAccount = amountToCreateAccount
|
||||
?.format(
|
||||
{
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
),
|
||||
balance = amount?.format(
|
||||
{
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
).orEmpty(),
|
||||
isTopUpDataLoading = status.value.networkAddress == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBalanceLoadingProgress(value: Boolean) {
|
||||
_uiState.update {
|
||||
it.copy(isRefreshing = value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
|
||||
modelScope.launch {
|
||||
val availableForBuy = rampStateManager.availableForBuy(
|
||||
scanResponse = scanResponse,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
availableForBuy = availableForBuy,
|
||||
availableForBuyLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) =
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = uiState.value.onDismissBottomSheet,
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
name = currencyStatus.currency.name,
|
||||
symbol = currencyStatus.currency.symbol,
|
||||
network = currencyStatus.currency.network.name,
|
||||
addresses = networkAddress.availableAddresses
|
||||
.mapToAddressModels(currencyStatus.currency)
|
||||
.toImmutableList(),
|
||||
showMemoDisclaimer =
|
||||
currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol))
|
||||
clipboardManager.setText(text = it, isSensitive = true)
|
||||
},
|
||||
onShareClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol))
|
||||
shareManager.shareText(text = it)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
val wallet = requireNotNull(
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
saveWalletUseCase(wallet, false)
|
||||
return wallet
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,44 @@
|
|||
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.foundation.shape.RoundedCornerShape
|
||||
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.draw.clip
|
||||
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.LocalTangemShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
|
||||
import com.tangem.features.onboarding.v2.common.ui.WalletCard
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
@Composable
|
||||
fun OnboardinNoteTopUpHeader(
|
||||
balance: String?,
|
||||
fun OnboardingNoteTopUpHeader(
|
||||
balance: String,
|
||||
cardArtworkUrl: String?,
|
||||
isRefreshing: Boolean,
|
||||
onRefreshBalanceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier
|
||||
.heightIn(min = 228.dp)
|
||||
.widthIn(max = 450.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 24.dp, horizontal = 32.dp)
|
||||
.padding(vertical = 24.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
TangemTheme.colors.button.secondary,
|
||||
|
|
@ -64,10 +62,17 @@ fun OnboardinNoteTopUpHeader(
|
|||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = balance.orEmpty(),
|
||||
modifier = if (balance.isEmpty()) {
|
||||
Modifier
|
||||
.width(120.dp)
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius3))
|
||||
.shimmer(LocalTangemShimmer.current)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
text = balance,
|
||||
)
|
||||
SpacerHMax()
|
||||
RefreshButton(
|
||||
|
|
@ -78,44 +83,11 @@ fun OnboardinNoteTopUpHeader(
|
|||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
OnboardingNoteTopUpHeader(
|
||||
balance = "0.00000001 BTC",
|
||||
cardArtworkUrl = "",
|
||||
onRefreshBalanceClick = {},
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
|
@ -10,14 +11,13 @@ 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.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
|
||||
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
|
||||
|
|
@ -27,23 +27,25 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif
|
|||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnboardinNoteTopUpHeader(
|
||||
OnboardingNoteTopUpHeader(
|
||||
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)
|
||||
.padding(top = 16.dp)
|
||||
.height(228.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(32.dp)
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(top = 16.dp)
|
||||
.weight(1.0f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
|
@ -55,8 +57,16 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif
|
|||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
|
||||
val text = if (state.amountToCreateAccount != null) {
|
||||
stringResourceSafe(
|
||||
R.string.onboarding_top_up_min_create_account_amount,
|
||||
state.amountToCreateAccount,
|
||||
)
|
||||
} else {
|
||||
stringResourceSafe(R.string.onboarding_top_up_body)
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_body),
|
||||
text = text,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -65,38 +75,43 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif
|
|||
}
|
||||
|
||||
BottomButtons(state)
|
||||
|
||||
state.addressBottomSheetConfig?.let { config ->
|
||||
TokenReceiveBottomSheet(config = config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomButtons(state: OnboardingNoteTopUpUM) {
|
||||
if (state.availableForBuy) {
|
||||
PrimaryButtonIconEnd(
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.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(
|
||||
PrimaryButton(
|
||||
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,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !state.availableForBuyLoading) {
|
||||
if (state.availableForBuy) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
||||
data class OnboardingNoteTopUpUM(
|
||||
val cardArtworkUrl: String? = null,
|
||||
val availableForBuy: Boolean = true,
|
||||
val balance: String? = null,
|
||||
val availableForBuy: Boolean = false,
|
||||
val availableForBuyLoading: Boolean = true,
|
||||
val balance: String = "",
|
||||
val isRefreshing: Boolean = false,
|
||||
val isTopUpDataLoading: Boolean = true,
|
||||
val amountToCreateAccount: String? = null,
|
||||
val addressBottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
val onBuyCryptoClick: () -> Unit = {},
|
||||
val onShowWalletAddressClick: () -> Unit = {},
|
||||
val onRefreshBalanceClick: () -> Unit = {},
|
||||
val onDismissBottomSheet: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.model
|
||||
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
internal data class OnboardingNoteCommonState(
|
||||
val scanResponse: ScanResponse,
|
||||
val userWallet: UserWallet? = null,
|
||||
val cardArtworkUrl: String? = null,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus? = null,
|
||||
)
|
||||
|
|
@ -4,46 +4,54 @@ 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.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog
|
||||
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 kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class OnboardingNoteModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val getCardImageUseCase: GetCardImageUseCase,
|
||||
) : Model() {
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private val params = paramsContainer.require<OnboardingNoteComponent.Params>()
|
||||
|
||||
private val _currentScanResponse = MutableStateFlow(params.scanResponse)
|
||||
|
||||
val initialRoute = OnboardingNoteRoute.CreateWallet // TODO [REDACTED_TASK_KEY]
|
||||
val initialRoute = initializeRoute()
|
||||
|
||||
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(),
|
||||
OnboardingNoteCommonState(
|
||||
scanResponse = params.scanResponse,
|
||||
),
|
||||
)
|
||||
|
||||
val uiState: StateFlow<OnboardingNoteState> = _uiState
|
||||
val commonUiState: MutableStateFlow<OnboardingNoteCommonState> = _uiState
|
||||
|
||||
init {
|
||||
loadArtworkUrl()
|
||||
}
|
||||
|
||||
fun updateStepForNewRoute(route: OnboardingNoteRoute) {
|
||||
_innerNavigationState.value = innerNavigationState.value.copy(
|
||||
|
|
@ -53,6 +61,40 @@ internal class OnboardingNoteModel @Inject constructor(
|
|||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
fun onChildBack(currentRoute: OnboardingNoteRoute) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
messageSender.send(
|
||||
exitOnboardingDialog(
|
||||
onConfirm = {
|
||||
router.pop()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onWalletCreated(userWallet: UserWallet) {
|
||||
commonUiState.update {
|
||||
it.copy(userWallet = userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeRoute(): OnboardingNoteRoute {
|
||||
val card = params.scanResponse.card
|
||||
return if (card.wallets.isEmpty()) {
|
||||
OnboardingNoteRoute.CreateWallet
|
||||
} else {
|
||||
OnboardingNoteRoute.TopUp
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadArtworkUrl() {
|
||||
modelScope.launch {
|
||||
val cardInfo = params.scanResponse.card
|
||||
val url = getCardImageUseCase(
|
||||
cardId = cardInfo.cardId,
|
||||
cardPublicKey = cardInfo.cardPublicKey,
|
||||
)
|
||||
_uiState.update {
|
||||
it.copy(cardArtworkUrl = url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.model
|
||||
|
||||
data class OnboardingNoteState(
|
||||
val cardArtworkUrl: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.onboarding.v2.twin.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.features.onboarding.v2.TitleProvider
|
||||
|
||||
interface OnboardingTwinComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val titleProvider: TitleProvider,
|
||||
val scanResponse: ScanResponse,
|
||||
val modelCallbacks: ModelCallbacks,
|
||||
val mode: Mode,
|
||||
) {
|
||||
enum class Mode {
|
||||
CreateWallet, RecreateWallet
|
||||
}
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingTwinComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: OnboardingTwinComponent.Params,
|
||||
) : OnboardingTwinComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnboardingTwinModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnboardingTwinComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnboardingTwinComponent.Params,
|
||||
): DefaultOnboardingTwinComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel
|
||||
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: DefaultOnboardingTwinComponent.Factory): OnboardingTwinComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnboardingTwinModel::class)
|
||||
fun provideModel(model: OnboardingTwinModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.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.twin.api.OnboardingTwinComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class OnboardingTwinModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
fun OnboardingTwin(modifier: Modifier = Modifier) {
|
||||
Column {
|
||||
Box {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
OnboardingTwin()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
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.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 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.domain.wallets.models.Artwork
|
||||
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
|
||||
import com.tangem.features.onboarding.v2.common.ui.WalletCard
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal sealed class TwinWalletArtworkState {
|
||||
data object Spread : TwinWalletArtworkState()
|
||||
|
||||
data class Leapfrog(
|
||||
val step: Step,
|
||||
) : TwinWalletArtworkState() {
|
||||
enum class Step {
|
||||
FirstCard, SecondCard
|
||||
}
|
||||
}
|
||||
|
||||
data object TopUp : TwinWalletArtworkState()
|
||||
}
|
||||
|
||||
private data class CardsTransitionState(
|
||||
val walletCard1: WalletCardTransitionState,
|
||||
val walletCard2: WalletCardTransitionState,
|
||||
)
|
||||
|
||||
private data class WalletCardTransitionState(
|
||||
val alpha: Float = 1f,
|
||||
val xTranslation: Float = 0f,
|
||||
val yTranslation: Float = 0f,
|
||||
val xScale: Float = 1f,
|
||||
val yScale: Float = 1f,
|
||||
val rotation: Float = 0f,
|
||||
val zIndex: Float = 0f,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TwinWalletArtworks(
|
||||
state: TwinWalletArtworkState,
|
||||
balance: String,
|
||||
isRefreshing: Boolean,
|
||||
onRefreshBalanceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier
|
||||
.heightIn(min = 180.dp)
|
||||
.widthIn(max = 450.dp),
|
||||
) {
|
||||
val animationWidth = maxWidth
|
||||
val density = LocalDensity.current.density
|
||||
|
||||
var animationState by remember {
|
||||
mutableStateOf(
|
||||
state.toTransitionSetState(
|
||||
maxWidthDp = animationWidth.value,
|
||||
density = density,
|
||||
maxHeightDp = maxHeight.value,
|
||||
).first(),
|
||||
)
|
||||
}
|
||||
|
||||
val transition1 = updateTransition(animationState.walletCard1, label = "transition")
|
||||
val transition2 = updateTransition(animationState.walletCard2, label = "transition")
|
||||
|
||||
LaunchedEffect(state) {
|
||||
val animationSetState = state.toTransitionSetState(
|
||||
maxWidthDp = animationWidth.value,
|
||||
density = density,
|
||||
maxHeightDp = maxHeight.value,
|
||||
)
|
||||
animationSetState.fastForEach {
|
||||
animationState = it
|
||||
val maxTime = maxOf(
|
||||
transition1.totalDurationNanos,
|
||||
transition2.totalDurationNanos,
|
||||
)
|
||||
delay(TimeUnit.NANOSECONDS.toMillis(maxTime))
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state == TwinWalletArtworkState.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 24.dp, horizontal = 32.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
TangemTheme.colors.button.secondary,
|
||||
shape = TangemTheme.shapes.roundedCornersMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedTwinCards(
|
||||
transition1 = transition1,
|
||||
transition2 = transition2,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 450.dp)
|
||||
.matchParentSize(),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = state == TwinWalletArtworkState.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
visible = state == TwinWalletArtworkState.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
RefreshButton(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefreshBalanceClick = onRefreshBalanceClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedTwinCards(
|
||||
transition1: Transition<WalletCardTransitionState>,
|
||||
transition2: Transition<WalletCardTransitionState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val alpha1 by transition1.animateFloat(label = "alpha1") { it.alpha }
|
||||
val translationX1 by transition1.animateFloat(label = "translationX1") { it.xTranslation }
|
||||
val translationY1 by transition1.animateFloat(label = "translationY1") { it.yTranslation }
|
||||
val rotation1 by transition1.animateFloat(label = "rotation1") { it.rotation }
|
||||
val scaleX1 by transition1.animateFloat(label = "scaleX1") { it.xScale }
|
||||
val scaleY1 by transition1.animateFloat(label = "scaleY1") { it.yScale }
|
||||
val zIndex1 = transition1.currentState.zIndex
|
||||
|
||||
val alpha2 by transition2.animateFloat(label = "alpha2") { it.alpha }
|
||||
val translationX2 by transition2.animateFloat(label = "translationX2") { it.xTranslation }
|
||||
val translationY2 by transition2.animateFloat(label = "translationY2") { it.yTranslation }
|
||||
val rotation2 by transition2.animateFloat(label = "rotation2") { it.rotation }
|
||||
val scaleX2 by transition2.animateFloat(label = "scaleX2") { it.xScale }
|
||||
val scaleY2 by transition2.animateFloat(label = "scaleY2") { it.yScale }
|
||||
val zIndex2 = transition2.currentState.zIndex
|
||||
|
||||
Box(modifier) {
|
||||
WalletCard(
|
||||
modifier = Modifier
|
||||
.zIndex(zIndex2)
|
||||
.matchParentSize()
|
||||
.graphicsLayer {
|
||||
translationX = translationX2
|
||||
translationY = translationY2
|
||||
}
|
||||
.graphicsLayer {
|
||||
alpha = alpha2
|
||||
rotationZ = rotation2
|
||||
scaleX = scaleX2
|
||||
scaleY = scaleY2
|
||||
},
|
||||
url = Artwork.TWIN_CARD_2_URL,
|
||||
)
|
||||
|
||||
WalletCard(
|
||||
modifier = Modifier
|
||||
.zIndex(zIndex1)
|
||||
.matchParentSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scaleX1
|
||||
scaleY = scaleY1
|
||||
}
|
||||
.graphicsLayer {
|
||||
alpha = alpha1
|
||||
translationX = translationX1
|
||||
translationY = translationY1
|
||||
rotationZ = rotation1
|
||||
},
|
||||
url = Artwork.TWIN_CARD_1_URL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses")
|
||||
private fun TwinWalletArtworkState.toTransitionSetState(
|
||||
maxWidthDp: Float,
|
||||
maxHeightDp: Float,
|
||||
density: Float,
|
||||
): List<CardsTransitionState> = when (this) {
|
||||
TwinWalletArtworkState.Spread -> {
|
||||
val scale = 0.8f
|
||||
listOf(
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(
|
||||
xTranslation = maxWidthDp * -0.15f * density,
|
||||
yTranslation = -65 * density,
|
||||
rotation = -5f,
|
||||
xScale = scale,
|
||||
yScale = scale,
|
||||
zIndex = 2f,
|
||||
),
|
||||
walletCard2 = WalletCardTransitionState(
|
||||
xTranslation = maxWidthDp * 0.15f * density,
|
||||
yTranslation = 65 * density,
|
||||
rotation = -5f,
|
||||
xScale = scale,
|
||||
yScale = scale,
|
||||
zIndex = 1f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
is TwinWalletArtworkState.Leapfrog -> {
|
||||
val translationY = -(maxHeightDp / 2) * density
|
||||
val secondCardPlace = WalletCardTransitionState().copy(
|
||||
xTranslation = 0f,
|
||||
yTranslation = ((maxHeightDp * (1 - 0.83f)) / 4 + 12) * density,
|
||||
rotation = 0f,
|
||||
xScale = 0.83f,
|
||||
yScale = 0.83f,
|
||||
)
|
||||
|
||||
if (step == TwinWalletArtworkState.Leapfrog.Step.FirstCard) {
|
||||
listOf(
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(
|
||||
yTranslation = translationY,
|
||||
zIndex = 2f,
|
||||
),
|
||||
walletCard2 = WalletCardTransitionState(),
|
||||
),
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(
|
||||
zIndex = 2f,
|
||||
),
|
||||
walletCard2 = secondCardPlace,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(),
|
||||
walletCard2 = WalletCardTransitionState(
|
||||
yTranslation = translationY,
|
||||
zIndex = 2f,
|
||||
),
|
||||
),
|
||||
CardsTransitionState(
|
||||
walletCard1 = secondCardPlace,
|
||||
walletCard2 = WalletCardTransitionState(
|
||||
zIndex = 2f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
TwinWalletArtworkState.TopUp -> {
|
||||
val scale = 0.4f
|
||||
val yTranslation = -maxHeightDp * density - 24 * density
|
||||
listOf(
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(
|
||||
yTranslation = yTranslation,
|
||||
xScale = scale,
|
||||
yScale = scale,
|
||||
zIndex = 2f,
|
||||
),
|
||||
walletCard2 = WalletCardTransitionState(
|
||||
yTranslation = yTranslation * 0.35f,
|
||||
xScale = scale * 0.8f,
|
||||
yScale = scale * 0.8f,
|
||||
zIndex = 1f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
var state: TwinWalletArtworkState by remember { mutableStateOf(TwinWalletArtworkState.TopUp) }
|
||||
|
||||
TwinWalletArtworks(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = 250.dp)
|
||||
.fillMaxWidth(),
|
||||
balance = "1 USD",
|
||||
isRefreshing = false,
|
||||
onRefreshBalanceClick = {},
|
||||
)
|
||||
|
||||
var index by remember { mutableIntStateOf(0) }
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.zIndex(5f)
|
||||
.align(Alignment.TopStart),
|
||||
onClick = {
|
||||
val list = listOf(
|
||||
TwinWalletArtworkState.Spread,
|
||||
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard),
|
||||
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard),
|
||||
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard),
|
||||
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard),
|
||||
TwinWalletArtworkState.TopUp,
|
||||
)
|
||||
|
||||
state = list[index % list.size]
|
||||
index++
|
||||
},
|
||||
) {
|
||||
Text("Animate $state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaOtherWalletComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.push(OnboardingVisaRoute.PinCode(activationOrderInfo = it))
|
||||
model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -174,7 +174,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaPinCodeComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.push(
|
||||
model.stackNavigation.pushNew(
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode),
|
||||
)
|
||||
},
|
||||
|
|
@ -190,7 +190,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaApproveComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.push(
|
||||
model.stackNavigation.pushNew(
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.tangem.core.ui.components.SpacerH
|
|||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -87,9 +88,9 @@ private fun Content(
|
|||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp),
|
||||
text = if (reEnterAccessCodeState) {
|
||||
"Re-enter your access code" // TODO
|
||||
stringResourceSafe(R.string.onboarding_access_code_repeat_code_title)
|
||||
} else {
|
||||
"Create access code" // TODO
|
||||
stringResourceSafe(R.string.onboarding_access_code_intro_title)
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -98,11 +99,10 @@ private fun Content(
|
|||
|
||||
SpacerH16()
|
||||
|
||||
// TODO
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp),
|
||||
text = "The access code will be used manage your payment account and protect it from unauthorized access",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_access_code_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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
|
||||
|
|
@ -43,7 +44,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod
|
|||
SpacerH(86.dp)
|
||||
|
||||
Text(
|
||||
text = "Prepare Tangem Wallet",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -52,7 +53,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod
|
|||
SpacerH(12.dp)
|
||||
|
||||
Text(
|
||||
text = "Prepare the Tangem card\nand tap to approve",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -65,7 +66,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod
|
|||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
primaryButton = NavigationButton(
|
||||
textReference = TextReference.Str("Approve"),
|
||||
textReference = resourceReference(R.string.common_approve),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isIconVisible = true,
|
||||
showProgress = state.approveButtonLoading,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model
|
|||
import androidx.compose.runtime.Stable
|
||||
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
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM
|
||||
|
|
@ -32,7 +32,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor(
|
|||
val selectedOption = SelectableChainRowUM(
|
||||
event = Event.TangemWallet,
|
||||
icon = R.drawable.ic_tangem_24,
|
||||
text = TextReference.Str("Tangem Wallet"),
|
||||
text = resourceReference(R.string.common_tangem_wallet),
|
||||
)
|
||||
|
||||
return OnboardingVisaChooseWalletUM(
|
||||
|
|
@ -41,7 +41,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor(
|
|||
SelectableChainRowUM(
|
||||
event = Event.OtherWallet,
|
||||
icon = R.drawable.ic_wallet_filled_24,
|
||||
text = TextReference.Str("Other Wallet"),
|
||||
text = resourceReference(R.string.visa_onboarding_other_wallet),
|
||||
),
|
||||
),
|
||||
selectedOption = selectedOption,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import com.tangem.core.ui.components.rows.RowContentContainer
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.outsetBorder
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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
|
||||
|
|
@ -45,10 +47,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod
|
|||
) {
|
||||
Notification(
|
||||
config = NotificationConfig(
|
||||
subtitle = TextReference.Str(
|
||||
"Please choose the wallet you started the registration" +
|
||||
" process with to sign the transaction for creating your account on the Blockchain.",
|
||||
),
|
||||
subtitle = resourceReference(R.string.visa_onboarding_approve_wallet_selector_notification_message),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
),
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
|
|
@ -62,7 +61,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod
|
|||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
),
|
||||
text = "Choose wallet",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_wallet_list_header),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
@ -79,7 +78,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod
|
|||
.imePadding()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Continue",
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import com.tangem.domain.visa.repository.VisaAuthRepository
|
|||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
|
||||
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
|
||||
|
|
@ -35,7 +34,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val otpStorage: VisaOTPStorage,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -110,9 +109,8 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
val newActivationStatus = VisaCardActivationStatus.Activated(visaAuthTokens = authTokens)
|
||||
|
||||
requireNotNull(
|
||||
value = UserWalletBuilder(
|
||||
scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
generateWalletNameUseCase,
|
||||
value = userWalletBuilderFactory.create(
|
||||
scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onShareClicked() {
|
||||
shareManager.shareText("https://tangem.com/")
|
||||
shareManager.shareText("https://tangem.com/") // TODO
|
||||
}
|
||||
|
||||
private fun onOpenInBrowserClicked() {
|
||||
urlOpener.openUrl("https://tangem.com/")
|
||||
urlOpener.openUrl("https://tangem.com/") // TODO
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ 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.components.SpacerH12
|
||||
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
|
||||
|
|
@ -44,7 +45,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif
|
|||
SpacerH12()
|
||||
|
||||
Text(
|
||||
text = "Go to Website",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -53,9 +54,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif
|
|||
SpacerH12()
|
||||
|
||||
Text(
|
||||
text = "You will be able to complete your connection \n" +
|
||||
"on the third-party web-site \n" +
|
||||
"and back to the Tangem app",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -68,7 +67,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif
|
|||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Open in Browser",
|
||||
text = stringResourceSafe(R.string.common_open_in_browser),
|
||||
onClick = state.onOpenInBrowserClick,
|
||||
)
|
||||
|
||||
|
|
@ -78,7 +77,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif
|
|||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Share Link",
|
||||
text = stringResourceSafe(R.string.common_share_link),
|
||||
onClick = state.onOpenInBrowserClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.extensions.toHexString
|
|||
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
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.SetVisaPinCodeUseCase
|
||||
|
|
@ -13,7 +14,7 @@ import com.tangem.domain.visa.model.VisaCardId
|
|||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -31,7 +32,7 @@ import javax.inject.Inject
|
|||
internal class OnboardingVisaPinCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val authTokenStorage: VisaAuthTokenStorage,
|
||||
private val otpStorage: VisaAuthTokenStorage,
|
||||
|
|
@ -57,24 +58,32 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onPinCodeChange(pin: String) {
|
||||
if (pin.all { it.isDigit() }) {
|
||||
if (PinCodeValidation.validateAllDigits(pin)) {
|
||||
val isError = PinCodeValidation.validateLength(pin) && PinCodeValidation.validate(pin).not()
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
pinCode = pin,
|
||||
submitButtonEnabled = checkPinCode(pin),
|
||||
submitButtonEnabled = PinCodeValidation.validate(pin),
|
||||
error = if (isError) {
|
||||
resourceReference(R.string.visa_onboarding_pin_validation_error_message)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubmitClick() {
|
||||
if (checkPinCode(_uiState.value.pinCode).not()) return
|
||||
val pinCode = _uiState.value.pinCode
|
||||
if (PinCodeValidation.validate(pinCode).not()) return
|
||||
|
||||
modelScope.launch {
|
||||
loading(true)
|
||||
|
||||
setVisaPinCodeUseCase(
|
||||
pinCode = _uiState.value.pinCode,
|
||||
pinCode = pinCode,
|
||||
visaCardId = visaCardId,
|
||||
activationOrderId = params.activationOrderInfo.orderId,
|
||||
).onLeft {
|
||||
|
|
@ -88,10 +97,6 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkPinCode(pin: String): Boolean {
|
||||
return pin.length == PIN_CODE_LENGTH
|
||||
}
|
||||
|
||||
private fun loading(state: Boolean) {
|
||||
_uiState.update { it.copy(submitButtonLoading = state) }
|
||||
}
|
||||
|
|
@ -111,15 +116,10 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
)
|
||||
|
||||
requireNotNull(
|
||||
value = UserWalletBuilder(
|
||||
scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
generateWalletNameUseCase,
|
||||
value = userWalletBuilderFactory.create(
|
||||
scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PIN_CODE_LENGTH = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model
|
||||
|
||||
internal object PinCodeValidation {
|
||||
|
||||
private const val PIN_CODE_LENGTH = 4
|
||||
|
||||
fun validate(pinCode: String): Boolean {
|
||||
return sequenceOf(
|
||||
::validateAllDigits,
|
||||
::validateLength,
|
||||
::validateNoRepeatedDigits,
|
||||
::validateNoConsecutiveDigits,
|
||||
).all { it(pinCode) }
|
||||
}
|
||||
|
||||
fun validateAllDigits(pinCode: String): Boolean {
|
||||
return pinCode.all { it.isDigit() }
|
||||
}
|
||||
|
||||
fun validateLength(pinCode: String): Boolean {
|
||||
return pinCode.length == PIN_CODE_LENGTH
|
||||
}
|
||||
|
||||
private fun validateNoRepeatedDigits(pinCode: String): Boolean {
|
||||
return pinCode.toSet().size == PIN_CODE_LENGTH
|
||||
}
|
||||
|
||||
private fun validateNoConsecutiveDigits(pinCode: String): Boolean {
|
||||
return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -31,9 +32,14 @@ import com.tangem.common.ui.navigationButtons.NavigationButton
|
|||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.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.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -53,7 +59,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "Create\nPIN Code",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_pin_code_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -62,7 +68,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod
|
|||
SpacerH16()
|
||||
|
||||
Text(
|
||||
text = "Set up a 4-digit code.\nIt will be used for payments.",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_pin_code_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -79,7 +85,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod
|
|||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
primaryButton = NavigationButton(
|
||||
textReference = TextReference.Str("Submit"),
|
||||
textReference = resourceReference(R.string.common_submit),
|
||||
onClick = state.onSubmitClick,
|
||||
showProgress = state.submitButtonLoading,
|
||||
isEnabled = state.submitButtonEnabled,
|
||||
|
|
@ -91,13 +97,29 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod
|
|||
@Composable
|
||||
private fun PinCodeSection(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PinCode(
|
||||
modifier = modifier,
|
||||
value = state.pinCode,
|
||||
onValueChange = state.onPinCodeChange,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
|
||||
PinCode(
|
||||
modifier = modifier,
|
||||
value = state.pinCode,
|
||||
onValueChange = state.onPinCodeChange,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = state.error != null,
|
||||
) {
|
||||
val error = remember(this) { requireNotNull(state.error) }
|
||||
Column {
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = error.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(timeMillis = 300)
|
||||
|
|
@ -134,9 +156,11 @@ private fun PinCode(
|
|||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
},),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
keyboardController?.hide()
|
||||
},
|
||||
),
|
||||
modifier = Modifier
|
||||
.alpha(alpha = 0.01f)
|
||||
.focusRequester(focusRequester),
|
||||
|
|
@ -194,6 +218,7 @@ private fun Preview() {
|
|||
OnboardingVisaPinCode(
|
||||
state = OnboardingVisaPinCodeUM(
|
||||
pinCode = "1234",
|
||||
error = stringReference("PIN Code error"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class OnboardingVisaPinCodeUM(
|
||||
val pinCode: String = "",
|
||||
val error: TextReference? = null,
|
||||
val onPinCodeChange: (String) -> Unit = {},
|
||||
val submitButtonLoading: Boolean = false,
|
||||
val submitButtonEnabled: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton
|
|||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
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
|
||||
|
|
@ -47,8 +48,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
|
|||
|
||||
Text(
|
||||
text = when (state.mode) {
|
||||
OnboardingVisaWelcomeUM.Mode.Hello -> "Hello, ${state.userName}!"
|
||||
OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "Welcome back, ${state.userName}!"
|
||||
OnboardingVisaWelcomeUM.Mode.Hello ->
|
||||
stringResourceSafe(R.string.visa_onboarding_welcome_title)
|
||||
OnboardingVisaWelcomeUM.Mode.WelcomeBack ->
|
||||
stringResourceSafe(R.string.visa_onboarding_welcome_back_title)
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -58,8 +61,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
|
|||
|
||||
Text(
|
||||
text = when (state.mode) {
|
||||
OnboardingVisaWelcomeUM.Mode.Hello -> "bla bla bla //TODO"
|
||||
OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "bla bla bla? //TODO"
|
||||
OnboardingVisaWelcomeUM.Mode.Hello ->
|
||||
stringResourceSafe(R.string.visa_onboarding_welcome_description)
|
||||
OnboardingVisaWelcomeUM.Mode.WelcomeBack ->
|
||||
stringResourceSafe(R.string.visa_onboarding_welcome_back_description)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
|
|
@ -72,7 +77,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
|
|||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Start Activation",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_welcome_button_title),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -81,7 +86,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
|
|||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Continue Activation",
|
||||
text = stringResourceSafe(R.string.visa_onboarding_welcome_back_button_title),
|
||||
showProgress = state.continueButtonLoading,
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = state.onContinueClick,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state
|
|||
|
||||
internal data class OnboardingVisaWelcomeUM(
|
||||
val mode: Mode = Mode.Hello,
|
||||
val userName: String = "",
|
||||
val continueButtonLoading: Boolean = false,
|
||||
val onContinueClick: () -> Unit = {},
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.route
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
// TODO add translations [REDACTED_TASK_KEY]
|
||||
internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) {
|
||||
OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code")
|
||||
is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
|
||||
is OnboardingVisaRoute.InProgress -> TextReference.Str("In progress")
|
||||
is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
|
||||
is OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
|
||||
is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
|
||||
OnboardingVisaRoute.AccessCode ->
|
||||
resourceReference(R.string.visa_onboarding_access_code_navigation_title)
|
||||
is OnboardingVisaRoute.ChooseWallet ->
|
||||
resourceReference(R.string.visa_onboarding_account_activation_navigation_title)
|
||||
is OnboardingVisaRoute.InProgress ->
|
||||
resourceReference(R.string.common_in_progress)
|
||||
is OnboardingVisaRoute.OtherWalletApproveOption,
|
||||
is OnboardingVisaRoute.TangemWalletApproveOption,
|
||||
-> resourceReference(R.string.visa_onboarding_wallet_connection_navigation_title)
|
||||
is OnboardingVisaRoute.PinCode ->
|
||||
resourceReference(R.string.visa_onboarding_pin_code_navigation_title)
|
||||
is OnboardingVisaRoute.Welcome,
|
||||
is OnboardingVisaRoute.WelcomeBack,
|
||||
-> TextReference.Str("Getting started")
|
||||
-> resourceReference(R.string.onboarding_getting_started)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) {
|
||||
childContent(modifier)
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
|
|||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.onramp.component.OnrampSuccessComponent
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents
|
||||
|
|
@ -42,6 +43,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
|
|||
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
|
||||
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
|
|
@ -79,17 +81,21 @@ internal class OnrampSuccessComponentModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun loadTransactionStatus(transaction: OnrampTransaction) {
|
||||
val multiCryptoCurrency = getCryptoCurrencyUseCase(
|
||||
transaction.userWalletId,
|
||||
transaction.toCurrencyId,
|
||||
).getOrNull()
|
||||
|
||||
val cryptoCurrency = multiCryptoCurrency
|
||||
?: getCryptoCurrencyUseCase.invoke(transaction.userWalletId).getOrElse {
|
||||
Timber.e("Crypto currency not found")
|
||||
showErrorAlert(OnrampError.DomainError(null))
|
||||
return
|
||||
}
|
||||
val userWallet = getUserWalletUseCase(transaction.userWalletId).getOrNull()
|
||||
if (userWallet == null) {
|
||||
Timber.e("UserWallet found")
|
||||
// this case should never happened
|
||||
showErrorAlert(OnrampError.DomainError("UserWallet not found"))
|
||||
return
|
||||
}
|
||||
val cryptoCurrency = getCryptoCurrencyUseCase(
|
||||
userWallet = userWallet,
|
||||
cryptoCurrencyId = transaction.toCurrencyId,
|
||||
).getOrElse {
|
||||
Timber.e("Crypto currency not found")
|
||||
showErrorAlert(OnrampError.DomainError(null))
|
||||
return
|
||||
}
|
||||
|
||||
getOnrampStatusUseCase(txId = transaction.txId)
|
||||
.fold(
|
||||
|
|
|
|||
1
features/send-v2/api/.gitignore
vendored
Normal file
1
features/send-v2/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
19
features/send-v2/api/build.gradle.kts
Normal file
19
features/send-v2/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.send.v2.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface SendComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val transactionId: String? = null,
|
||||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
interface SendFeatureToggles {
|
||||
val isSendV2Enabled: Boolean
|
||||
}
|
||||
1
features/send-v2/impl/.gitignore
vendored
Normal file
1
features/send-v2/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
70
features/send-v2/impl/build.gradle.kts
Normal file
70
features/send-v2/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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.send.v2.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Tangem SDK */
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.qrScanning.models)
|
||||
implementation(projects.domain.qrScanning)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.txhistory)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.send.v2
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
|
||||
internal class DefaultSendFeatureToggles(
|
||||
private val featureToggles: FeatureTogglesManager,
|
||||
) : SendFeatureToggles {
|
||||
override val isSendV2Enabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("SEND_V2_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.v2.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.v2.DefaultSendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.send.DefaultSendComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal object SendFeatureModule {
|
||||
|
||||
@Provides
|
||||
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles {
|
||||
return DefaultSendFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SendFeatureModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.v2.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface SendModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendModel::class)
|
||||
fun provideSendModel(model: SendModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendAmountModel::class)
|
||||
fun provideSendAmountModel(model: SendAmountModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendDestinationModel::class)
|
||||
fun provideSendDestinationModel(model: SendDestinationModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.tangem.features.send.v2.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
internal class DefaultSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendComponent.Params,
|
||||
) : SendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<SendRoute>()
|
||||
|
||||
private val innerRouter = InnerRouter<SendRoute>(
|
||||
stackNavigation = stackNavigation,
|
||||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val initialRoute = SendRoute.Empty
|
||||
private val currentRoute = MutableStateFlow<SendRoute>(initialRoute)
|
||||
|
||||
private val model: SendModel = getOrCreateModel(params = params, router = innerRouter)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "sendInnerStack",
|
||||
source = stackNavigation,
|
||||
serializer = null,
|
||||
initialConfiguration = initialRoute,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
createChild(
|
||||
configuration,
|
||||
childByContext(
|
||||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
}
|
||||
|
||||
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
SendRoute.Empty -> getStubComponent()
|
||||
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
|
||||
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
|
||||
is SendRoute.Fee -> getFeeComponent()
|
||||
SendRoute.Confirm -> getConfirmComponent()
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
|
||||
SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponent.Params(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
|
||||
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
isEditMode = route.isEditMode,
|
||||
),
|
||||
)
|
||||
|
||||
private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendAmountComponentParams.AmountParams(
|
||||
state = model.uiState.value.amountUM,
|
||||
currentRoute = currentRoute.filterIsInstance<SendRoute.Amount>(),
|
||||
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
|
||||
userWallet = model.userWallet,
|
||||
appCurrency = model.appCurrency,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
isEditMode = route.isEditMode,
|
||||
predefinedAmountValue = model.predefinedAmountValue,
|
||||
),
|
||||
)
|
||||
|
||||
private fun getFeeComponent() = getStubComponent() // todo
|
||||
|
||||
private fun getConfirmComponent() = getStubComponent() // todo
|
||||
|
||||
private fun getStubComponent() = ComposableContentComponent { }
|
||||
|
||||
private fun onChildBack() {
|
||||
if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) {
|
||||
router.pop()
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: SendComponent.Params): DefaultSendComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.v2.send
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class SendRoute : Route {
|
||||
|
||||
abstract val isEditMode: Boolean
|
||||
|
||||
@Serializable
|
||||
data object Empty : SendRoute() {
|
||||
override val isEditMode = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Confirm : SendRoute() {
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Destination(
|
||||
override val isEditMode: Boolean,
|
||||
) : SendRoute()
|
||||
|
||||
@Serializable
|
||||
data class Amount(
|
||||
override val isEditMode: Boolean,
|
||||
) : SendRoute()
|
||||
|
||||
@Serializable
|
||||
data class Fee(
|
||||
override val isEditMode: Boolean = true,
|
||||
) : SendRoute()
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.send.v2.send.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
/**
|
||||
* Send screen analytics
|
||||
*/
|
||||
internal sealed class SendAnalyticEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) {
|
||||
|
||||
/** Recipient address screen opened */
|
||||
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
|
||||
|
||||
/** Amount screen opened */
|
||||
data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
|
||||
|
||||
/** Fee screen opened */
|
||||
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
|
||||
|
||||
/** Confirmation screen opened */
|
||||
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
|
||||
|
||||
/** If transaction delays notification is present */
|
||||
data class NoticeTransactionDelays(
|
||||
val token: String,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Notice - Transaction Delays Are Possible",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
/** If error occurs during send transactions */
|
||||
data class TransactionError(val token: String) : SendAnalyticEvents(
|
||||
event = "Error - Transaction Rejected",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val SEND_CATEGORY = "Token / Send"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.features.send.v2.send.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model(),
|
||||
SendDestinationComponent.ModelCallback,
|
||||
SendAmountComponent.ModelCallback {
|
||||
|
||||
private val _uiState = MutableStateFlow(initialState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
var userWallet: UserWallet by Delegates.notNull()
|
||||
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
var appCurrency: AppCurrency = AppCurrency.Default
|
||||
var predefinedAmountValue: String? = null
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onAmountResult(state: AmountState) {
|
||||
_uiState.update { it.copy(amountUM = state) }
|
||||
}
|
||||
|
||||
private fun initialState(): SendUM = SendUM(
|
||||
amountUM = AmountState.Empty(),
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.send.v2.send.ui.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
|
||||
internal data class SendUM(
|
||||
val amountUM: AmountState,
|
||||
val destinationUM: DestinationUM,
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
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.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class SendAmountBlockComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: AmountBlockParams,
|
||||
val onResult: (AmountState) -> Unit,
|
||||
val onClick: () -> Unit,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendAmountModel = getOrCreateModel(params = params, router = router)
|
||||
|
||||
init {
|
||||
model.uiState.onEach {
|
||||
onResult(it)
|
||||
}.launchIn(componentScope)
|
||||
}
|
||||
|
||||
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle()
|
||||
val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle()
|
||||
|
||||
AmountBlock(
|
||||
amountState = state.value,
|
||||
isClickDisabled = !isClickEnabled.value,
|
||||
isEditingDisabled = isEditingDisabled.value,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenContent
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
|
||||
|
||||
internal class SendAmountComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: AmountParams,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendAmountModel = getOrCreateModel(params = params)
|
||||
|
||||
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AmountScreenContent(
|
||||
amountState = state,
|
||||
isBalanceHidden = false,
|
||||
clickIntents = model,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onAmountResult(amountUM: AmountState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal sealed class SendAmountComponentParams {
|
||||
|
||||
abstract val state: AmountState
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val userWallet: UserWallet
|
||||
abstract val appCurrency: AppCurrency
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
|
||||
data class AmountParams(
|
||||
override val state: AmountState,
|
||||
override val analyticsCategoryName: String,
|
||||
override val userWallet: UserWallet,
|
||||
override val appCurrency: AppCurrency,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val isEditMode: Boolean,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<SendRoute.Amount>,
|
||||
val predefinedAmountValue: String?,
|
||||
) : SendAmountComponentParams()
|
||||
|
||||
data class AmountBlockParams(
|
||||
override val state: AmountState,
|
||||
override val analyticsCategoryName: String,
|
||||
override val userWallet: UserWallet,
|
||||
override val appCurrency: AppCurrency,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val blockEditDisabledFlow: StateFlow<Boolean>,
|
||||
val blockClickEnableFlow: StateFlow<Boolean>,
|
||||
) : SendAmountComponentParams()
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Trigger for reducing amount from another component
|
||||
*/
|
||||
interface SendAmountReduceTrigger {
|
||||
suspend fun triggerReduceBy(reduceBy: ReduceByData)
|
||||
suspend fun triggerReduceTo(reduceTo: BigDecimal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger for reducing amount from another component
|
||||
*/
|
||||
interface SendAmountReduceListener {
|
||||
val reduceToTriggerFlow: Flow<BigDecimal>
|
||||
val reduceByTriggerFlow: Flow<ReduceByData>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
|
||||
SendAmountReduceTrigger,
|
||||
SendAmountReduceListener {
|
||||
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
|
||||
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
|
||||
|
||||
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
|
||||
reduceByTriggerFlow.emit(reduceBy)
|
||||
}
|
||||
|
||||
override suspend fun triggerReduceTo(reduceTo: BigDecimal) {
|
||||
reduceToTriggerFlow.emit(reduceTo)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
|
||||
internal sealed class SendAmountAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
/** Selected currency */
|
||||
data class SelectedCurrency(
|
||||
val categoryName: String,
|
||||
val type: SelectedCurrencyType,
|
||||
) : SendAmountAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Selected Currency",
|
||||
params = mapOf(TYPE to type.value),
|
||||
)
|
||||
|
||||
/** Max amount button clicked */
|
||||
data class MaxAmountButtonClicked(
|
||||
val categoryName: String,
|
||||
) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped")
|
||||
|
||||
internal enum class SelectedCurrencyType(val value: String) {
|
||||
Token("Token"),
|
||||
AppCurrency("App Currency"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.di
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal object SendAmountModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendAmountReduceTrigger(): SendAmountReduceTrigger {
|
||||
return DefaultSendAmountReduceTrigger()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendAmountReduceListener(): SendAmountReduceListener {
|
||||
return DefaultSendAmountReduceTrigger()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.converters.*
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
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.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents
|
||||
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SendAmountModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
|
||||
private val sendAmountReduceListener: SendAmountReduceListener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), AmountScreenClickIntents {
|
||||
|
||||
private val params: SendAmountComponentParams = paramsContainer.require()
|
||||
|
||||
private val _uiState = MutableStateFlow(params.state)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private val userWallet = params.userWallet
|
||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
|
||||
private var minAmountBoundary: EnterAmountBoundary? = null
|
||||
private var maxAmountBoundary: EnterAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus)
|
||||
|
||||
init {
|
||||
configAmountNavigation()
|
||||
initMinBoundary()
|
||||
subscribeOnAmountReduceByTriggerUpdates()
|
||||
subscribeOnAmountReduceToTriggerUpdates()
|
||||
}
|
||||
|
||||
private fun initMinBoundary() {
|
||||
modelScope.launch {
|
||||
minAmountBoundary = getMinimumTransactionAmountSyncUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
).getOrNull()?.let {
|
||||
EnterAmountBoundary(
|
||||
amount = it,
|
||||
fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(),
|
||||
)
|
||||
}
|
||||
|
||||
initialState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue
|
||||
if (uiState.value is AmountState.Empty) {
|
||||
_uiState.update {
|
||||
AmountStateConverterV2(
|
||||
clickIntents = this,
|
||||
appCurrency = params.appCurrency,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxEnterAmount = maxAmountBoundary,
|
||||
iconStateConverter = CryptoCurrencyToIconStateConverter(),
|
||||
).convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWallet.name),
|
||||
value = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
if (predefinedAmountValue != null) {
|
||||
onAmountValueChange(predefinedAmountValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateState(amountUM: AmountState) {
|
||||
if (amountUM !is AmountState.Empty) {
|
||||
_uiState.value = amountUM
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
_uiState.update(AmountCurrencyTransformer(cryptoCurrencyStatus, isFiat))
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
_uiState.update(
|
||||
AmountFieldChangeTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxEnterAmount = maxAmountBoundary,
|
||||
minimumTransactionAmount = minAmountBoundary,
|
||||
value = value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val decimalCryptoValue = cryptoCurrencyStatus.value.amount
|
||||
if (decimalCryptoValue.isNullOrZero()) return
|
||||
|
||||
_uiState.update(
|
||||
AmountFieldSetMaxAmountTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = maxAmountBoundary,
|
||||
minAmount = minAmountBoundary,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {
|
||||
_uiState.update(AmountPastedTriggerDismissTransformer)
|
||||
}
|
||||
|
||||
override fun onAmountNext() {
|
||||
(uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected ->
|
||||
analyticsEventHandler.send(
|
||||
SendAmountAnalyticEvents.SelectedCurrency(
|
||||
categoryName = analyticsCategoryName,
|
||||
type = if (isFiatSelected) {
|
||||
SelectedCurrencyType.AppCurrency
|
||||
} else {
|
||||
SelectedCurrencyType.Token
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
saveResult()
|
||||
if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) {
|
||||
router.pop()
|
||||
} else {
|
||||
router.push(SendRoute.Confirm)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnAmountReduceToTriggerUpdates() {
|
||||
sendAmountReduceListener.reduceToTriggerFlow
|
||||
.onEach { reduceTo ->
|
||||
_uiState.update(
|
||||
AmountReduceToTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minAmountBoundary,
|
||||
value = reduceTo,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAmountReduceByTriggerUpdates() {
|
||||
sendAmountReduceListener.reduceByTriggerFlow
|
||||
.onEach { reduceByData ->
|
||||
_uiState.update(
|
||||
AmountReduceByTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minAmountBoundary,
|
||||
value = reduceByData,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
val params = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
params.callback.onAmountResult(uiState.value)
|
||||
}
|
||||
|
||||
private fun configAmountNavigation() {
|
||||
val params = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (_, _) ->
|
||||
// todo
|
||||
// params.callback.onNavigationResult(
|
||||
// NavigationUM.Content(
|
||||
// title = resourceReference(R.string.send_amount_label),
|
||||
// subtitle = null,
|
||||
// backIconRes = R.drawable.ic_back_24,
|
||||
// backIconClick = {
|
||||
// if (route.isEditMode) {
|
||||
// saveResult()
|
||||
// }
|
||||
// router.pop()
|
||||
// },
|
||||
// primaryButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = if (route.isEditMode) {
|
||||
// resourceReference(R.string.common_continue)
|
||||
// } else {
|
||||
// resourceReference(R.string.common_next)
|
||||
// },
|
||||
// isEnabled = state.isPrimaryButtonEnabled,
|
||||
// onClick = ::onAmountNext,
|
||||
// ),
|
||||
// prevButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = TextReference.EMPTY,
|
||||
// iconResId = R.drawable.ic_back_24,
|
||||
// isEnabled = true,
|
||||
// onClick = {
|
||||
// saveResult()
|
||||
// router.pop()
|
||||
// },
|
||||
// ).takeIf { route.isEditMode.not() },
|
||||
// secondaryPairButtonsUM = null,
|
||||
// ),
|
||||
// )
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class SendDestinationComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendDestinationModel = getOrCreateModel(params = params, router = router)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SendDestinationContent(state = state.value, model, isBalanceHidden = false)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: DestinationUM,
|
||||
val analyticsCategoryName: String,
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<SendRoute.Destination>,
|
||||
val isEditMode: Boolean,
|
||||
)
|
||||
|
||||
interface ModelCallback {
|
||||
fun onDestinationResult(destinationUM: DestinationUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.analytics
|
||||
|
||||
internal enum class EnterAddressSource {
|
||||
QRCode,
|
||||
PasteButton,
|
||||
RecentAddress,
|
||||
InputField,
|
||||
;
|
||||
|
||||
val isPasted: Boolean
|
||||
get() = this != InputField
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
|
||||
|
||||
internal sealed class SendDestinationAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
abstract val categoryName: String
|
||||
|
||||
/** Address to send entered */
|
||||
data class AddressEntered(
|
||||
override val categoryName: String,
|
||||
val source: EnterAddressSource,
|
||||
val isValid: Boolean,
|
||||
) : SendDestinationAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Address Entered",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
VALIDATION to if (isValid) "Success" else "Fail",
|
||||
),
|
||||
)
|
||||
|
||||
/** Qr Code button clicked */
|
||||
data class QrCodeButtonClicked(
|
||||
override val categoryName: String,
|
||||
) :
|
||||
SendDestinationAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Button - QR Code",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
|
||||
internal interface SendDestinationClickIntents {
|
||||
|
||||
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false)
|
||||
|
||||
fun onQrCodeScanClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
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.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.*
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendDestinationModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), SendDestinationClickIntents {
|
||||
private val params: SendDestinationComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _uiState = MutableStateFlow(params.state)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private val userWallet = params.userWallet
|
||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
private val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
private var validationJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
configDestinationNavigation()
|
||||
initialState()
|
||||
getWalletsAndRecent()
|
||||
subscribeOnQRScannerResult()
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
if (uiState.value is DestinationUM.Empty) {
|
||||
_uiState.update(
|
||||
SendDestinationInitialStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onAddressChange = ::onRecipientAddressValueChange,
|
||||
onMemoChange = ::onRecipientMemoValueChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateState(state: DestinationUM) {
|
||||
if (state !is DestinationUM.Empty) {
|
||||
_uiState.value = state
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) {
|
||||
_uiState.update(
|
||||
SendDestinationAddressTransformer(
|
||||
address = value,
|
||||
isPasted = type.isPasted,
|
||||
),
|
||||
)
|
||||
val memo = (uiState.value as? DestinationUM.Content)?.memoTextField?.value
|
||||
validate(address = value, memo = memo, type)
|
||||
}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
||||
_uiState.update(
|
||||
SendDestinationMemoTransformer(
|
||||
memo = value,
|
||||
isPasted = isValuePasted,
|
||||
),
|
||||
)
|
||||
val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.value.orEmpty()
|
||||
validate(address = address, memo = value)
|
||||
}
|
||||
|
||||
override fun onQrCodeScanClick() {
|
||||
analyticsEventHandler.send(SendDestinationAnalyticEvents.QrCodeButtonClicked(analyticsCategoryName))
|
||||
router.push(
|
||||
AppRoute.QrScanning(
|
||||
source = AppRoute.QrScanning.Source.SEND,
|
||||
networkName = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnQRScannerResult() {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
.onEach(::onQrCodeScanned)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onQrCodeScanned(address: String) {
|
||||
parseQrCodeUseCase(address, cryptoCurrency).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
ifLeft = {
|
||||
onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getWalletsUseCase().conflate().map {
|
||||
it.toAvailableWallets()
|
||||
},
|
||||
flow2 = getFixedTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
pageSize = RECENT_TX_SIZE,
|
||||
).getOrElse { flowOf(emptyList()) }.conflate(),
|
||||
) { destinationWalletList, txHistoryList ->
|
||||
val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
_uiState.update(
|
||||
SendDestinationRecentListTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable,
|
||||
destinationWalletList = destinationWalletList,
|
||||
txHistoryList,
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.toAvailableWallets(): List<DestinationWalletUM> {
|
||||
return coroutineScope {
|
||||
val cryptoCurrencyNetwork = cryptoCurrency.network
|
||||
|
||||
return@coroutineScope filterNot { it.isLocked }
|
||||
.map { wallet ->
|
||||
async {
|
||||
val addresses = if (!wallet.isMultiCurrency) {
|
||||
getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let {
|
||||
if (it.network.id == cryptoCurrencyNetwork.id) {
|
||||
getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrencyNetwork)
|
||||
}
|
||||
wallet to addresses
|
||||
}
|
||||
}.awaitAll()
|
||||
.asSequence()
|
||||
.mapNotNull { (wallet, addresses) ->
|
||||
addresses?.map { (cryptoCurrency, address) ->
|
||||
DestinationWalletUM(
|
||||
name = wallet.name,
|
||||
address = address,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = wallet.walletId,
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) {
|
||||
modelScope.launch {
|
||||
_uiState.update(SendDestinationValidationStartedTransformer)
|
||||
|
||||
val addressValidationResult = validateWalletAddressUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = address,
|
||||
currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses,
|
||||
)
|
||||
val memoValidationResult = validateWalletMemoUseCase(
|
||||
memo = memo.orEmpty(),
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
type?.let {
|
||||
analyticsEventHandler.send(
|
||||
SendDestinationAnalyticEvents.AddressEntered(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = it,
|
||||
isValid = addressValidationResult.isRight(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_uiState.update(SendDestinationValidationResultTransformer(addressValidationResult, memoValidationResult))
|
||||
autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight())
|
||||
}.saveIn(validationJobHolder)
|
||||
}
|
||||
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) {
|
||||
val isRecent = type == EnterAddressSource.RecentAddress
|
||||
if (isRecent && isValidAddress && isValidMemo) onNextClick()
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
params.callback.onDestinationResult(uiState.value)
|
||||
}
|
||||
|
||||
private fun onNextClick() {
|
||||
saveResult()
|
||||
if (params.isEditMode) {
|
||||
router.pop()
|
||||
} else {
|
||||
router.push(SendRoute.Amount(isEditMode = false))
|
||||
}
|
||||
}
|
||||
|
||||
private fun configDestinationNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach {
|
||||
// todo
|
||||
// params.callback.onNavigationResult(
|
||||
// NavigationUM.Content(
|
||||
// title = resourceReference(R.string.send_recipient_label),
|
||||
// subtitle = null,
|
||||
// backIconRes = if (route.isEditMode) {
|
||||
// R.drawable.ic_back_24
|
||||
// } else {
|
||||
// R.drawable.ic_close_24
|
||||
// },
|
||||
// backIconClick = {
|
||||
// if (route.isEditMode) {
|
||||
// router.pop()
|
||||
// } else {
|
||||
// appRouter.pop()
|
||||
// }
|
||||
// },
|
||||
// additionalIconRes = R.drawable.ic_qrcode_scan_24,
|
||||
// additionalIconClick = {
|
||||
// router.push(
|
||||
// AppRoute.QrScanning(
|
||||
// source = AppRoute.QrScanning.Source.SEND,
|
||||
// networkName = cryptoCurrency.network.name,
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
// primaryButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = if (route.isEditMode) {
|
||||
// resourceReference(R.string.common_continue)
|
||||
// } else {
|
||||
// resourceReference(R.string.common_next)
|
||||
// },
|
||||
// isEnabled = state.isPrimaryButtonEnabled,
|
||||
// onClick = ::onNextClick,
|
||||
// ),
|
||||
// prevButton = null,
|
||||
// secondaryPairButtonsUM = null,
|
||||
// ),
|
||||
// )
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RECENT_TX_SIZE = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_DEFAULT_COUNT
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_KEY_TAG
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientHistoryListConverter(
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Converter<List<TxHistoryItem>, ImmutableList<DestinationRecipientListUM>> {
|
||||
|
||||
override fun convert(value: List<TxHistoryItem>): ImmutableList<DestinationRecipientListUM> {
|
||||
return value.filterRecipients(cryptoCurrency).ifEmpty {
|
||||
emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item ->
|
||||
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
|
||||
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
|
||||
val isSingleAddress = if (item.isOutgoing) {
|
||||
item.destinationType is TxHistoryItem.DestinationType.Single
|
||||
} else {
|
||||
item.sourceType is TxHistoryItem.SourceType.Single
|
||||
}
|
||||
val notZero = !item.amount.isZero()
|
||||
isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero
|
||||
}
|
||||
.take(RECENT_LIST_SIZE)
|
||||
.mapIndexed { index, tx ->
|
||||
DestinationRecipientListUM(
|
||||
id = "${RECENT_KEY_TAG}$index",
|
||||
title = tx.extractAddress(),
|
||||
subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()),
|
||||
timestamp = tx.extractTimestamp(),
|
||||
subtitleEndOffset = cryptoCurrency.symbol.length,
|
||||
subtitleIconRes = tx.extractIconRes(),
|
||||
)
|
||||
}.toPersistentList()
|
||||
|
||||
private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
|
||||
when (val destination = destinationType) {
|
||||
is TxHistoryItem.DestinationType.Multiple -> resourceReference(
|
||||
R.string.transaction_history_multiple_addresses,
|
||||
)
|
||||
is TxHistoryItem.DestinationType.Single -> stringReference(destination.addressType.address)
|
||||
}
|
||||
} else {
|
||||
when (val source = sourceType) {
|
||||
is TxHistoryItem.SourceType.Multiple -> resourceReference(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.SourceType.Single -> stringReference(source.address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIconRes() = if (isOutgoing) {
|
||||
R.drawable.ic_arrow_up_24
|
||||
} else {
|
||||
R.drawable.ic_arrow_down_24
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String {
|
||||
return amount.format { crypto(cryptoCurrency) }
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
val date = timestampInMillis.toDateFormatWithTodayYesterday(
|
||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||
)
|
||||
val time = timestampInMillis.toTimeFormat()
|
||||
return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RECENT_LIST_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientWalletListConverter(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
) :
|
||||
Converter<List<DestinationWalletUM?>, PersistentList<DestinationRecipientListUM>> {
|
||||
override fun convert(value: List<DestinationWalletUM?>): PersistentList<DestinationRecipientListUM> {
|
||||
return value.filterWallets().ifEmpty {
|
||||
emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DestinationWalletUM?>.filterWallets(): PersistentList<DestinationRecipientListUM> {
|
||||
var walletsCounter = 0
|
||||
val currentAddress: String = runCatching {
|
||||
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
}.getOrNull().orEmpty()
|
||||
|
||||
return this.filterNotNull()
|
||||
.filter {
|
||||
val isCoin = it.cryptoCurrency is CryptoCurrency.Coin
|
||||
val isNotSameAddress = it.address != currentAddress
|
||||
val isNotBlankAddress = it.address.isNotBlank()
|
||||
|
||||
isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable)
|
||||
}
|
||||
.groupBy { item -> item.name }
|
||||
.values.map { wallets ->
|
||||
val groupedByWallet = wallets.groupBy { it.userWalletId }
|
||||
var i = 0
|
||||
groupedByWallet
|
||||
.flatMap { item ->
|
||||
item.value.map { wallet ->
|
||||
val name = if (groupedByWallet.size > 1) {
|
||||
"${wallet.name} ${++i}"
|
||||
} else {
|
||||
wallet.name
|
||||
}
|
||||
|
||||
DestinationRecipientListUM(
|
||||
id = "${WALLET_KEY_TAG}${walletsCounter++}",
|
||||
title = stringReference(wallet.address),
|
||||
subtitle = stringReference(name),
|
||||
address = wallet.address,
|
||||
userWalletId = wallet.userWalletId,
|
||||
network = wallet.cryptoCurrency.network,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal const val WALLET_DEFAULT_COUNT = 1
|
||||
internal const val RECENT_DEFAULT_COUNT = 3
|
||||
internal const val WALLET_KEY_TAG = "wallet"
|
||||
internal const val RECENT_KEY_TAG = "recent"
|
||||
|
||||
internal fun loadingListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
DestinationRecipientListUM(
|
||||
id = "$tag$it",
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
internal fun emptyListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
DestinationRecipientListUM(
|
||||
id = "$tag$it",
|
||||
isLoading = false,
|
||||
isVisible = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationAddressTransformer(
|
||||
private val address: String,
|
||||
private val isPasted: Boolean,
|
||||
) : Transformer<DestinationUM> {
|
||||
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(
|
||||
addressTextField = state.addressTextField.copy(value = address, isValuePasted = isPasted),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationInitialStateTransformer(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val onAddressChange: (String, EnterAddressSource) -> Unit,
|
||||
val onMemoChange: (String, Boolean) -> Unit,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) {
|
||||
Network.TransactionExtrasType.NONE -> null
|
||||
Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo
|
||||
Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field
|
||||
}
|
||||
return DestinationUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
addressTextField = DestinationTextFieldUM.RecipientAddress(
|
||||
value = "",
|
||||
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
error = null,
|
||||
placeholder = resourceReference(R.string.send_enter_address_field),
|
||||
label = resourceReference(R.string.send_recipient),
|
||||
isValuePasted = false,
|
||||
),
|
||||
memoTextField = memoType?.let {
|
||||
DestinationTextFieldUM.RecipientMemo(
|
||||
value = "",
|
||||
onValueChange = { onMemoChange(it, false) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = resourceReference(R.string.send_optional_field),
|
||||
label = resourceReference(memoType),
|
||||
error = resourceReference(R.string.send_memo_destination_tag_error),
|
||||
disabledText = resourceReference(R.string.send_additional_field_already_included),
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
)
|
||||
},
|
||||
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
|
||||
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
|
||||
networkName = cryptoCurrencyStatus.currency.network.name,
|
||||
isValidating = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationMemoTransformer(
|
||||
private val memo: String,
|
||||
private val isPasted: Boolean,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
return state.copy(
|
||||
memoTextField = state.memoTextField?.copy(value = memo, isValuePasted = isPasted),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationRecentListTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
private val destinationWalletList: List<DestinationWalletUM>,
|
||||
private val txHistoryList: List<TxHistoryItem>,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(
|
||||
wallets = SendRecipientWalletListConverter(
|
||||
cryptoCurrencyStatus,
|
||||
isUtxoConsolidationAvailable,
|
||||
).convert(destinationWalletList),
|
||||
recent = SendRecipientHistoryListConverter(
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
).convert(txHistoryList),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.error.ValidateMemoError
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationValidationResultTransformer(
|
||||
private val addressValidationResult: AddressValidationResult,
|
||||
private val memoValidationResult: Either<ValidateMemoError, Unit>,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
val isValidAddress = addressValidationResult.isRight()
|
||||
val isValidMemo = memoValidationResult.isRight()
|
||||
|
||||
val addressErrorText = addressValidationResult.mapLeft {
|
||||
when (it) {
|
||||
is AddressValidation.Error.DataError,
|
||||
AddressValidation.Error.InvalidAddress,
|
||||
-> R.string.send_recipient_address_error
|
||||
AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet
|
||||
}
|
||||
}.leftOrNull()
|
||||
|
||||
return state.copy(
|
||||
isValidating = false,
|
||||
isPrimaryButtonEnabled = isValidAddress && isValidMemo,
|
||||
addressTextField = state.addressTextField.copy(
|
||||
error = addressErrorText?.let(::resourceReference),
|
||||
isError = state.addressTextField.value.isNotEmpty() && !isValidAddress,
|
||||
),
|
||||
memoTextField = state.memoTextField?.copy(
|
||||
isError = state.memoTextField.value.isNotEmpty() && !isValidMemo,
|
||||
isEnabled = !shouldDisableMemo(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** Ripple X-Address contains memo, so memo field is unnecessary */
|
||||
private fun shouldDisableMemo(): Boolean {
|
||||
return addressValidationResult.isRight { it == AddressValidation.Success.ValidXAddress }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object SendDestinationValidationStartedTransformer : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(isValidating = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
|
||||
/**
|
||||
* Row item with title and subtitle
|
||||
*
|
||||
* @param title title
|
||||
* @param subtitle subtitle
|
||||
* @param onClick click listener
|
||||
* @param modifier modifier
|
||||
* @param info info
|
||||
* @param subtitleEndOffset offset for subtitle ellipsis
|
||||
* @param subtitleIconRes icon
|
||||
*/
|
||||
@Composable
|
||||
fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
subtitleEndOffset: Int = 0,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = isLoading,
|
||||
label = "Recent List Content Animation",
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
) { isLoadingState ->
|
||||
if (isLoadingState) {
|
||||
ListItemLoading(modifier = modifier)
|
||||
} else {
|
||||
ListItemWithIcon(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
onClick = onClick,
|
||||
info = info,
|
||||
subtitleEndOffset = subtitleEndOffset,
|
||||
subtitleIconRes = subtitleIconRes,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
subtitleEndOffset: Int = 0,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
) {
|
||||
val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { hapticFeedback() }
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
IdentIcon(
|
||||
address = title,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp)),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
EllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Row {
|
||||
if (subtitleIconRes != null) {
|
||||
Icon(
|
||||
painter = painterResource(id = subtitleIconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(end = 2.dp)
|
||||
.size(16.dp)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.padding(2.dp),
|
||||
)
|
||||
}
|
||||
val (text, offset) = remember(subtitle, info) {
|
||||
if (info != null) {
|
||||
val suffix = ", $info"
|
||||
subtitle + suffix to suffix.length + subtitleEndOffset
|
||||
} else {
|
||||
subtitle to 0
|
||||
}
|
||||
}
|
||||
EllipsisText(
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListItemLoading(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
CircleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.size(40.dp),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
radius = 3.dp,
|
||||
modifier = Modifier.size(
|
||||
width = 70.dp,
|
||||
height = 12.dp,
|
||||
),
|
||||
)
|
||||
RectangleShimmer(
|
||||
radius = 3.dp,
|
||||
modifier = Modifier.size(
|
||||
width = 52.dp,
|
||||
height = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ListItemWithIconPreview(
|
||||
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
subtitleEndOffset = config.subtitleEndOffset,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
isLoading = config.isLoading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ListItemWithIconPreviewConfig(
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val info: String? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
val iconRes: Int? = null,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider<ListItemWithIconPreviewConfig>(
|
||||
collection = listOf(
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "1 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "Wallet",
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
isLoading = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.components.inputrow.InputRowRecipient
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
|
||||
private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
|
||||
|
||||
@Composable
|
||||
internal fun SendDestinationContent(
|
||||
state: DestinationUM,
|
||||
clickIntents: SendDestinationClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
if (state !is DestinationUM.Content) return
|
||||
val recipients = state.recent
|
||||
val wallets = state.wallets
|
||||
val memoField = state.memoTextField
|
||||
val address = state.addressTextField
|
||||
val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } }
|
||||
val isError by remember(address.isError) { derivedStateOf { address.isError } }
|
||||
LazyColumn(
|
||||
modifier = Modifier // Do not put fillMaxSize() in here
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
addressItem(
|
||||
address = address,
|
||||
networkName = state.networkName,
|
||||
isError = isError,
|
||||
isValidating = isValidating,
|
||||
onAddressChange = clickIntents::onRecipientAddressValueChange,
|
||||
)
|
||||
memoField(
|
||||
memoField = memoField,
|
||||
onMemoChange = clickIntents::onRecipientMemoValueChange,
|
||||
)
|
||||
listHeaderItem(
|
||||
titleRes = R.string.send_recipient_wallets_title,
|
||||
isVisible = wallets.isNotEmpty() && wallets.first().isVisible,
|
||||
isFirst = true,
|
||||
)
|
||||
listItem(
|
||||
list = wallets,
|
||||
clickIntents = clickIntents,
|
||||
isLast = recipients.any { !it.isVisible },
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
listHeaderItem(
|
||||
titleRes = R.string.send_recent_transactions,
|
||||
isVisible = recipients.isNotEmpty() && recipients.first().isVisible,
|
||||
isFirst = wallets.any { !it.isVisible },
|
||||
)
|
||||
listItem(
|
||||
list = recipients,
|
||||
clickIntents = clickIntents,
|
||||
isLast = true,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.addressItem(
|
||||
address: DestinationTextFieldUM.RecipientAddress,
|
||||
networkName: String,
|
||||
isError: Boolean,
|
||||
isValidating: Boolean,
|
||||
onAddressChange: (String, EnterAddressSource) -> Unit,
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
FooterContainer(
|
||||
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)),
|
||||
) {
|
||||
InputRowRecipient(
|
||||
value = address.value,
|
||||
title = address.label,
|
||||
placeholder = address.placeholder,
|
||||
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
|
||||
onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) },
|
||||
isError = isError,
|
||||
isLoading = isValidating,
|
||||
error = address.error,
|
||||
isValuePasted = address.isValuePasted,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.memoField(
|
||||
memoField: DestinationTextFieldUM.RecipientMemo?,
|
||||
onMemoChange: (String, Boolean) -> Unit,
|
||||
) {
|
||||
if (memoField != null) {
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText
|
||||
TextFieldWithPaste(
|
||||
value = memoField.value,
|
||||
label = memoField.label,
|
||||
placeholder = placeholder,
|
||||
footer = resourceReference(R.string.send_recipient_memo_footer),
|
||||
onValueChange = { onMemoChange(it, false) },
|
||||
onPasteClick = { onMemoChange(it, true) },
|
||||
modifier = Modifier.padding(top = 20.dp),
|
||||
labelStyle = TangemTheme.typography.subtitle2,
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
isReadOnly = !memoField.isEnabled,
|
||||
isValuePasted = memoField.isValuePasted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) {
|
||||
item(key = titleRes) {
|
||||
AnimateRecentAppearance(isVisible) {
|
||||
val (topPadding, paddingFromTop) = if (isFirst) {
|
||||
20.dp to 12.dp
|
||||
} else {
|
||||
0.dp to 8.dp
|
||||
}
|
||||
val topRadius = if (isFirst) 16.dp else 0.dp
|
||||
Text(
|
||||
text = stringResourceSafe(titleRes),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = topPadding)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = topRadius,
|
||||
topStart = topRadius,
|
||||
),
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
top = paddingFromTop,
|
||||
bottom = 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.listItem(
|
||||
list: ImmutableList<DestinationRecipientListUM>,
|
||||
clickIntents: SendDestinationClickIntents,
|
||||
isLast: Boolean,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
items(
|
||||
count = list.size,
|
||||
key = { list[it].id },
|
||||
contentType = { list[it]::class.java },
|
||||
) { index ->
|
||||
val item = list[index]
|
||||
val title = item.title.resolveReference()
|
||||
|
||||
if (item.isVisible) {
|
||||
ListItemWithIcon(
|
||||
title = title,
|
||||
subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
info = item.timestamp?.resolveReference(),
|
||||
subtitleEndOffset = item.subtitleEndOffset,
|
||||
subtitleIconRes = item.subtitleIconRes,
|
||||
onClick = {
|
||||
clickIntents.onRecipientAddressValueChange(
|
||||
title,
|
||||
EnterAddressSource.RecentAddress,
|
||||
)
|
||||
},
|
||||
isLoading = item.isLoading,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.then(
|
||||
if (isLast && index == list.lastIndex) {
|
||||
Modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 16.dp,
|
||||
bottomEnd = 16.dp,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) {
|
||||
AnimatedContent(
|
||||
targetState = isVisible,
|
||||
label = "Item Appearance Animation",
|
||||
transitionSpec = {
|
||||
(slideInHorizontally() + fadeIn())
|
||||
.togetherWith(slideOutVertically() + fadeOut())
|
||||
},
|
||||
) {
|
||||
if (it) {
|
||||
content()
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue