Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-27 12:45:57 +03:00
parent 10bb769916
commit bb32839f9d
32 changed files with 521 additions and 77 deletions

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.settings)
implementation(projects.domain.onboarding)
implementation(projects.domain.visa)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)

View file

@ -52,6 +52,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
MutableSharedFlow<Unit>()
}
private val childParams = ChildParams(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
)
private val stackNavigation = StackNavigation<OnboardingVisaRoute>()
private val childStack: Value<ChildStack<OnboardingVisaRoute, ComposableContentComponent>> =
@ -76,7 +81,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) {
componentScope.launch { currentChildBackEventHandle.emit(Unit) }
} else {
stackNavigation.pop()
onChildBack()
}
}
}
@ -90,11 +95,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
}
}
private val childParams = ChildParams(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
)
@Suppress("LongMethod")
private fun createChild(
route: OnboardingVisaRoute,
factoryContext: AppComponentContext,
@ -112,10 +113,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
appComponentContext = factoryContext,
params = OnboardingVisaAccessCodeComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.ChooseWallet) },
onDone = {
stackNavigation.push(
if (it.walletFound) {
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = it.visaDataForApprove,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(it.visaDataForApprove)
},
)
},
),
config = OnboardingVisaAccessCodeComponent.Config(
scanResponse = params.scanResponse,
),
)
OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
is OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
appComponentContext = factoryContext,
params = OnboardingVisaChooseWalletComponent.Params(
childParams = childParams,
@ -123,9 +138,12 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
stackNavigation.push(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = route.visaDataForApprove,
allowNavigateBack = true,
)
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption
OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove)
},
)
},
@ -138,8 +156,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) },
),
)
OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
appComponentContext = factoryContext,
config = OnboardingVisaOtherWalletComponent.Config(
visaDataForApprove = route.visaDataForApprove,
),
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
@ -152,8 +173,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
onDone = { params.onDone() },
),
)
OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
is OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
appComponentContext = factoryContext,
config = OnboardingVisaApproveComponent.Config(
visaDataForApprove = route.visaDataForApprove,
),
params = OnboardingVisaApproveComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
@ -163,15 +187,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
}
private fun onChildBack() {
when (childStack.value.active.configuration) {
if (childStack.value.backStack.size == 1) {
// TODO show dialog
return
}
when (val route = childStack.value.active.configuration) {
OnboardingVisaRoute.AccessCode,
OnboardingVisaRoute.TangemWalletApproveOption,
OnboardingVisaRoute.OtherWalletApproveOption,
is OnboardingVisaRoute.OtherWalletApproveOption,
-> stackNavigation.pop()
is OnboardingVisaRoute.TangemWalletApproveOption -> {
if (route.allowNavigateBack) {
stackNavigation.pop()
}
}
is OnboardingVisaRoute.Welcome,
OnboardingVisaRoute.ChooseWallet,
is OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
OnboardingVisaRoute.OtherWalletApproveOption,
OnboardingVisaRoute.PinCode,
-> {
}

View file

@ -9,6 +9,8 @@ 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.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode
@ -16,10 +18,11 @@ import kotlinx.coroutines.launch
internal class OnboardingVisaAccessCodeComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaAccessCodeModel = getOrCreateModel()
private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(config)
init {
componentScope.launch {
@ -31,7 +34,7 @@ internal class OnboardingVisaAccessCodeComponent(
}
}
componentScope.launch {
model.onDone.collect { params.onDone() }
model.onDone.collect { params.onDone(it) }
}
}
@ -46,8 +49,17 @@ internal class OnboardingVisaAccessCodeComponent(
OnboardingVisaAccessCode(state, modifier)
}
data class Config(
val scanResponse: ScanResponse,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,
val onDone: (DoneEvent) -> Unit,
)
data class DoneEvent(
val visaDataForApprove: VisaDataForApprove,
val walletFound: Boolean,
)
}

View file

@ -2,8 +2,15 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -14,15 +21,22 @@ import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
) : Model() {
private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require()
private val activationStatus =
params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation
?: error("Visa activation status is not set or incorrect for this step")
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onBack = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<OnboardingVisaAccessCodeComponent.DoneEvent>()
fun onBack() {
when (uiState.value.step) {
@ -97,27 +111,53 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
@Suppress("UnusedPrivateMember")
private fun startActivationProcess(accessCode: String) {
modelScope.launch { onDone.emit(Unit) }
loading(true)
// modelScope.launch {
// @Suppress("UnusedPrivateMember")
// val result = tangemSdkManager.activateVisaCard(
// accessCode = accessCode,
// challengeToSign = null,
// activationInput = TODO(),
// )
//
// when (result) {
// is CompletionResult.Success -> {
// val response = result.data
// TODO()
// }
// is CompletionResult.Failure -> {
// // show alert
// TODO()
// }
// }
// }
modelScope.launch {
val challengeToSign = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = activationStatus.activationInput.cardId,
cardPublicKey = activationStatus.activationInput.cardPublicKey.toHexString(),
)
}.getOrElse {
loading(false)
// show alert
return@launch
}
val result = tangemSdkManager.activateVisaCard(
accessCode = accessCode,
challengeToSign = challengeToSign,
activationInput = activationStatus.activationInput,
)
//
when (result) {
is CompletionResult.Success -> {
// TODO load approve data from backend
// TODO try to find wallet in the app
modelScope.launch {
onDone.emit(
OnboardingVisaAccessCodeComponent.DoneEvent(
visaDataForApprove = VisaDataForApprove(
targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6",
approveHash = "approve hash",
),
walletFound = false, // TODO
),
)
}
}
is CompletionResult.Failure -> {
loading(false)
// show alert
}
}
}
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(buttonLoading = state) }
}
private companion object {

View file

@ -65,6 +65,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie
},
onClick = state.onContinue,
iconRes = R.drawable.ic_tangem_24,
showProgress = state.buttonLoading,
isIconVisible = state.step == OnboardingVisaAccessCodeUM.Step.ReEnter,
),
)

View file

@ -9,6 +9,7 @@ internal data class OnboardingVisaAccessCodeUM(
val codesNotMatchError: Boolean = false,
val atLeastMinCharsError: Boolean = false,
val accessCodeHidden: Boolean = true,
val buttonLoading: Boolean = false,
val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {},
val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {},
val onContinue: () -> Unit = {},

View file

@ -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.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.OnboardingVisaApprove
@ -16,10 +17,11 @@ import kotlinx.coroutines.launch
internal class OnboardingVisaApproveComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaApproveModel = getOrCreateModel()
private val model: OnboardingVisaApproveModel = getOrCreateModel(config)
init {
componentScope.launch {
@ -39,6 +41,10 @@ internal class OnboardingVisaApproveComponent(
)
}
data class Config(
val visaDataForApprove: VisaDataForApprove,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,

View file

@ -1,22 +1,30 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaApproveModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
) : Model() {
private val params = paramsContainer.require<OnboardingVisaApproveComponent.Config>()
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
@ -29,7 +37,29 @@ internal class OnboardingVisaApproveModel @Inject constructor(
}
private fun onApproveClick() {
// TODO
modelScope.launch { onDone.emit(Unit) }
loading(true)
modelScope.launch {
val result = tangemSdkManager.visaCustomerWalletApprove(
visaDataForApprove = params.visaDataForApprove,
)
when (result) {
is CompletionResult.Success -> {
// TODO make backend call
onDone.emit(Unit)
}
is CompletionResult.Failure -> {
loading(false)
// TODO show dialog
return@launch
}
}
}
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(approveButtonLoading = state) }
}
}

View file

@ -68,6 +68,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod
textReference = TextReference.Str("Approve"),
iconRes = R.drawable.ic_tangem_24,
isIconVisible = true,
showProgress = state.approveButtonLoading,
onClick = state.onApproveClick,
),
)

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state
internal data class OnboardingVisaApproveUM(
val approveButtonLoading: Boolean = false,
val onApproveClick: () -> Unit = {},
)

View file

@ -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.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.OnboardingVisaOtherWallet
@ -17,10 +18,11 @@ import kotlinx.coroutines.flow.onEach
internal class OnboardingVisaOtherWalletComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaOtherWalletModel = getOrCreateModel()
private val model: OnboardingVisaOtherWalletModel = getOrCreateModel(config)
init {
model.onDone
@ -40,6 +42,10 @@ internal class OnboardingVisaOtherWalletComponent(
)
}
data class Config(
val visaDataForApprove: VisaDataForApprove,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,

View file

@ -3,6 +3,8 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
@ -13,9 +15,12 @@ import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaOtherWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val config = paramsContainer.require<OnboardingVisaOtherWalletComponent.Config>()
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -95,16 +96,18 @@ fun Artwork(modifier: Modifier = Modifier) {
Box(modifier) {
val circleColor = TangemTheme.colors.background.secondary
var cardHeightPx by remember { mutableIntStateOf(0) }
val circleRadiusPx = cardHeightPx / 2 + cardHeightPx / 8f
val circleSize = with(LocalDensity.current) { (circleRadiusPx * 2).toDp() }
Canvas(
modifier = Modifier
.align(Alignment.Center)
.size(254.dp)
.size(circleSize)
.fillMaxSize(),
) {
drawCircle(
color = circleColor,
radius = cardHeightPx / 2 + cardHeightPx / 8f,
radius = circleRadiusPx,
center = center,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.route
import com.tangem.domain.visa.model.VisaDataForApprove
import kotlinx.serialization.Serializable
@Serializable
@ -12,13 +13,16 @@ sealed class OnboardingVisaRoute {
data object AccessCode : OnboardingVisaRoute()
@Serializable
data object ChooseWallet : OnboardingVisaRoute()
data class ChooseWallet(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
@Serializable
data object TangemWalletApproveOption : OnboardingVisaRoute()
data class TangemWalletApproveOption(
val visaDataForApprove: VisaDataForApprove,
val allowNavigateBack: Boolean,
) : OnboardingVisaRoute()
@Serializable
data object OtherWalletApproveOption : OnboardingVisaRoute()
data class OtherWalletApproveOption(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
@Serializable
data object InProgress : OnboardingVisaRoute()

View file

@ -4,9 +4,9 @@ package com.tangem.features.onboarding.v2.visa.impl.route
internal fun OnboardingVisaRoute.stepNum() = when (this) {
is OnboardingVisaRoute.Welcome -> 1
OnboardingVisaRoute.AccessCode -> 2
OnboardingVisaRoute.ChooseWallet -> 3
OnboardingVisaRoute.OtherWalletApproveOption -> 4
OnboardingVisaRoute.TangemWalletApproveOption -> 4
is OnboardingVisaRoute.ChooseWallet -> 3
is OnboardingVisaRoute.OtherWalletApproveOption -> 4
is OnboardingVisaRoute.TangemWalletApproveOption -> 4
OnboardingVisaRoute.InProgress -> 5
OnboardingVisaRoute.PinCode -> 6
}

View file

@ -5,10 +5,10 @@ import com.tangem.core.ui.extensions.TextReference
// TODO add translations [REDACTED_TASK_KEY]
internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) {
OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code")
OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
OnboardingVisaRoute.InProgress -> TextReference.Str("In progress")
OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started")
}

View file

@ -0,0 +1,9 @@
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)
}